valtech-components 2.0.506 → 2.0.508

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 (38) hide show
  1. package/esm2022/lib/components/atoms/qr-code/qr-code.component.mjs +6 -4
  2. package/esm2022/lib/components/molecules/ad-slot/ad-slot.component.mjs +96 -39
  3. package/esm2022/lib/components/molecules/code-display/code-display.component.mjs +4 -2
  4. package/esm2022/lib/components/molecules/date-input/date-input.component.mjs +18 -7
  5. package/esm2022/lib/components/molecules/date-range-input/date-range-input.component.mjs +41 -16
  6. package/esm2022/lib/components/molecules/file-input/file-input.component.mjs +17 -6
  7. package/esm2022/lib/components/molecules/language-selector/language-selector.component.mjs +5 -5
  8. package/esm2022/lib/components/molecules/multi-select-search/multi-select-search.component.mjs +53 -27
  9. package/esm2022/lib/components/molecules/participant-card/participant-card.component.mjs +28 -10
  10. package/esm2022/lib/components/molecules/popover-selector/popover-selector.component.mjs +8 -6
  11. package/esm2022/lib/components/molecules/searchbar/searchbar.component.mjs +21 -7
  12. package/esm2022/lib/components/molecules/select-input/select-input.component.mjs +11 -7
  13. package/esm2022/lib/components/molecules/select-search/select-search.component.mjs +21 -7
  14. package/esm2022/lib/components/organisms/data-table/data-table.component.mjs +52 -17
  15. package/esm2022/lib/services/i18n/config.mjs +64 -4
  16. package/esm2022/lib/services/i18n/default-content.mjs +203 -0
  17. package/esm2022/lib/services/i18n/index.mjs +3 -1
  18. package/esm2022/lib/services/i18n/types.mjs +2 -1
  19. package/fesm2022/valtech-components.mjs +1180 -708
  20. package/fesm2022/valtech-components.mjs.map +1 -1
  21. package/lib/components/atoms/qr-code/qr-code.component.d.ts +1 -0
  22. package/lib/components/molecules/ad-slot/ad-slot.component.d.ts +5 -0
  23. package/lib/components/molecules/code-display/code-display.component.d.ts +2 -2
  24. package/lib/components/molecules/date-input/date-input.component.d.ts +5 -0
  25. package/lib/components/molecules/date-range-input/date-range-input.component.d.ts +11 -0
  26. package/lib/components/molecules/file-input/file-input.component.d.ts +3 -0
  27. package/lib/components/molecules/multi-select-search/multi-select-search.component.d.ts +13 -0
  28. package/lib/components/molecules/participant-card/participant-card.component.d.ts +9 -0
  29. package/lib/components/molecules/popover-selector/popover-selector.component.d.ts +1 -0
  30. package/lib/components/molecules/searchbar/searchbar.component.d.ts +14 -1
  31. package/lib/components/molecules/select-input/select-input.component.d.ts +1 -0
  32. package/lib/components/molecules/select-search/select-search.component.d.ts +7 -0
  33. package/lib/components/organisms/article/article.component.d.ts +1 -1
  34. package/lib/components/organisms/data-table/data-table.component.d.ts +17 -2
  35. package/lib/services/i18n/default-content.d.ts +30 -0
  36. package/lib/services/i18n/index.d.ts +1 -0
  37. package/lib/services/i18n/types.d.ts +35 -1
  38. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { EventEmitter, Component, Input, Output, Injectable, signal, makeEnvironmentProviders, APP_INITIALIZER, inject, HostListener, Pipe, ChangeDetectionStrategy, ViewChild, computed, ChangeDetectorRef, ElementRef, PLATFORM_ID, Inject, ErrorHandler, DestroyRef, InjectionToken, Optional, runInInjectionContext, effect } from '@angular/core';
2
+ import { EventEmitter, Component, Input, Output, Injectable, signal, makeEnvironmentProviders, APP_INITIALIZER, inject, HostListener, Pipe, ChangeDetectionStrategy, computed, ViewChild, ChangeDetectorRef, ElementRef, PLATFORM_ID, Inject, ErrorHandler, DestroyRef, InjectionToken, Optional, runInInjectionContext, effect } from '@angular/core';
3
3
  import * as i2$1 from '@ionic/angular/standalone';
4
4
  import { 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, IonDatetimeButton, IonInput, IonSelect, IonSelectOption, IonPopover, IonList, IonItem, IonRadioGroup, IonRadio, IonRange, IonSearchbar, IonSegment, IonSegmentButton, IonToggle, IonAccordion, IonAccordionGroup, IonTabBar, IonTabButton, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonChip, IonNote, ToastController as ToastController$1, IonCol, IonRow, IonMenuButton, IonFooter, IonListHeader, IonInfiniteScroll, IonInfiniteScrollContent, IonGrid, MenuController, IonMenu, IonMenuToggle, AlertController } from '@ionic/angular/standalone';
5
5
  import * as i1 from '@angular/common';
@@ -3120,136 +3120,703 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
3120
3120
  }]
3121
3121
  }] });
3122
3122
 
3123
- addIcons({ downloadOutline, copyOutline, shareOutline });
3124
3123
  /**
3125
- * val-qr-code
3124
+ * Valores por defecto de configuración
3125
+ */
3126
+ const DEFAULT_I18N_CONFIG = {
3127
+ defaultLanguage: 'es',
3128
+ supportedLanguages: ['es', 'en'],
3129
+ detectBrowserLanguage: true,
3130
+ content: {},
3131
+ includeDefaultContent: true,
3132
+ };
3133
+ /**
3134
+ * Clave para persistir idioma en localStorage
3135
+ */
3136
+ const LANG_STORAGE_KEY$1 = 'app_lang';
3137
+
3138
+ /**
3139
+ * Servicio de internacionalización basado en Angular Signals.
3126
3140
  *
3127
- * A component to display QR codes generated by QrGeneratorService.
3128
- * Provides optional action buttons for download, copy, and share.
3141
+ * Características:
3142
+ * - Sin RxJS: usa Signals para evitar memory leaks y congelamiento
3143
+ * - Namespace-based: organiza traducciones por contexto
3144
+ * - Fallback multi-nivel: namespace → _global → placeholder
3145
+ * - Interpolación: soporta {variable} en textos
3129
3146
  *
3130
- * @example Basic usage
3131
- * ```typescript
3132
- * qr = await this.qrService.generate({ data: 'https://example.com' });
3133
- * ```
3134
- * ```html
3135
- * <val-qr-code [props]="{ qr: qr }"></val-qr-code>
3136
- * ```
3147
+ * @example
3148
+ * // En un componente
3149
+ * i18n = inject(I18nService);
3137
3150
  *
3138
- * @example With actions
3139
- * ```html
3140
- * <val-qr-code
3141
- * [props]="{
3142
- * qr: qr,
3143
- * showDownload: true,
3144
- * showCopy: true,
3145
- * showShare: true,
3146
- * displaySize: 200,
3147
- * showBorder: true,
3148
- * borderRadius: 12
3149
- * }"
3150
- * (actionComplete)="onAction($event)"
3151
- * ></val-qr-code>
3152
- * ```
3151
+ * // Obtener texto
3152
+ * const title = this.i18n.t('title', 'Login');
3153
3153
  *
3154
- * @input props: QrCodeMetadata - Configuration for the QR display
3155
- * @output actionComplete - Emits when an action (download/copy/share) completes
3156
- * @output imageLoad - Emits when the QR image loads
3157
- * @output imageError - Emits when the QR image fails to load
3154
+ * // Con interpolación
3155
+ * const welcome = this.i18n.t('welcome', 'Login', { name: 'Juan' });
3156
+ *
3157
+ * // Cambiar idioma
3158
+ * this.i18n.setLanguage('en');
3158
3159
  */
3159
- class QrCodeComponent {
3160
+ class I18nService {
3160
3161
  constructor() {
3161
- this.actionComplete = new EventEmitter();
3162
- this.imageLoad = new EventEmitter();
3163
- this.imageError = new EventEmitter();
3164
- this.canShare = false;
3165
- this.canCopy = false;
3166
- this.qrService = inject(QrGeneratorService);
3162
+ // Estado interno con Signals
3163
+ this._lang = signal(DEFAULT_I18N_CONFIG.defaultLanguage);
3164
+ this._content = signal({});
3165
+ this._supportedLanguages = signal(DEFAULT_I18N_CONFIG.supportedLanguages);
3166
+ // Públicos readonly
3167
+ this.lang = this._lang.asReadonly();
3168
+ this.supportedLanguages = this._supportedLanguages.asReadonly();
3169
+ // Computed para verificaciones rápidas
3170
+ this.isSpanish = computed(() => this._lang() === 'es');
3171
+ this.isEnglish = computed(() => this._lang() === 'en');
3172
+ this.loadStoredLanguage();
3167
3173
  }
3168
- ngOnInit() {
3169
- this.canShare = this.qrService.canShare();
3170
- this.canCopy = this.qrService.canCopyToClipboard();
3174
+ /**
3175
+ * Obtiene texto traducido (alias corto de getText)
3176
+ *
3177
+ * @param key Clave del texto
3178
+ * @param namespace Namespace (default: '_global')
3179
+ * @param data Variables para interpolación
3180
+ * @returns Texto traducido o placeholder [namespace.key]
3181
+ *
3182
+ * @example
3183
+ * i18n.t('submit'); // busca en _global
3184
+ * i18n.t('title', 'Login'); // busca en Login
3185
+ * i18n.t('welcome', 'Login', {name}); // con interpolación
3186
+ */
3187
+ t(key, namespace, data) {
3188
+ return this.getText(key, namespace, data);
3171
3189
  }
3172
- getDisplaySize() {
3173
- if (this.props.displaySize) {
3174
- return `${this.props.displaySize}px`;
3190
+ /**
3191
+ * Obtiene texto traducido
3192
+ *
3193
+ * Fallback order:
3194
+ * 1. content[namespace][lang][key]
3195
+ * 2. content['_global'][lang][key]
3196
+ * 3. "[namespace.key]" (placeholder)
3197
+ */
3198
+ getText(key, namespace, data) {
3199
+ const content = this._content();
3200
+ const lang = this._lang();
3201
+ const ns = namespace || '_global';
3202
+ // Buscar en namespace específico
3203
+ let text = content[ns]?.[lang]?.[key];
3204
+ // Fallback a _global
3205
+ if (!text && ns !== '_global') {
3206
+ text = content['_global']?.[lang]?.[key];
3175
3207
  }
3176
- return `${this.props.qr.config.width || 300}px`;
3177
- }
3178
- getBorderRadius() {
3179
- return this.props.borderRadius ? `${this.props.borderRadius}px` : '0';
3180
- }
3181
- getPadding() {
3182
- return this.props.padding ? `${this.props.padding}px` : '0';
3183
- }
3184
- getQrBorderRadius() {
3185
- return this.props.qrBorderRadius ? `${this.props.qrBorderRadius}px` : '0';
3186
- }
3187
- getContainerClasses() {
3188
- const classes = [];
3189
- if (this.props.cssClass)
3190
- classes.push(this.props.cssClass);
3191
- if (this.props.showBorder)
3192
- classes.push('with-border');
3193
- if (this.props.loading)
3194
- classes.push('loading');
3195
- if (this.props.theme)
3196
- classes.push(`theme--${this.props.theme}`);
3197
- if (this.props.pulseOnHover)
3198
- classes.push('pulse-on-hover');
3199
- if (this.props.scaleOnHover)
3200
- classes.push('scale-on-hover');
3201
- if (this.props.gradient)
3202
- classes.push('has-gradient');
3203
- return classes.join(' ');
3204
- }
3205
- getContainerBackground() {
3206
- if (this.props.gradient) {
3207
- const { from, to, via, direction = 'to-bottom' } = this.props.gradient;
3208
- const cssDirection = direction.replace(/-/g, ' ');
3209
- if (via) {
3210
- return `linear-gradient(${cssDirection}, ${from}, ${via}, ${to})`;
3211
- }
3212
- return `linear-gradient(${cssDirection}, ${from}, ${to})`;
3208
+ // Fallback a placeholder
3209
+ if (!text) {
3210
+ console.warn(`[i18n] Missing translation: ${ns}.${key} (${lang})`);
3211
+ return `[${ns}.${key}]`;
3213
3212
  }
3214
- return this.props.containerBackground || 'transparent';
3213
+ // Aplicar interpolación si hay data
3214
+ if (data) {
3215
+ return this.interpolate(text, data);
3216
+ }
3217
+ return text;
3215
3218
  }
3216
- getShadow() {
3217
- const shadowMap = {
3218
- none: 'none',
3219
- sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
3220
- md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
3221
- lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
3222
- xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
3223
- };
3224
- if (!this.props.shadow)
3225
- return 'none';
3226
- return shadowMap[this.props.shadow] || this.props.shadow;
3219
+ /**
3220
+ * Cambia el idioma de la aplicación
3221
+ *
3222
+ * @param lang Nuevo idioma
3223
+ * @param forceReload Si true, recarga la página (fallback si reactividad falla)
3224
+ */
3225
+ setLanguage(lang, forceReload = false) {
3226
+ if (!this._supportedLanguages().includes(lang)) {
3227
+ console.warn(`[i18n] Language '${lang}' not in supported languages`);
3228
+ return;
3229
+ }
3230
+ if (lang === this._lang()) {
3231
+ return;
3232
+ }
3233
+ // Persistir en localStorage
3234
+ localStorage.setItem(LANG_STORAGE_KEY$1, lang);
3235
+ // Actualizar signal
3236
+ this._lang.set(lang);
3237
+ // Fallback: recargar si se solicita
3238
+ if (forceReload) {
3239
+ window.location.reload();
3240
+ }
3227
3241
  }
3228
- getLabelFontSize() {
3229
- return this.props.label?.fontSize ? `${this.props.label.fontSize}px` : '14px';
3242
+ /**
3243
+ * Registra contenido de traducciones para un namespace
3244
+ *
3245
+ * @param namespace Nombre del namespace
3246
+ * @param content Contenido de traducciones
3247
+ *
3248
+ * @example
3249
+ * i18n.registerContent('Login', {
3250
+ * es: { title: 'Iniciar sesión' },
3251
+ * en: { title: 'Sign in' }
3252
+ * });
3253
+ */
3254
+ registerContent(namespace, content) {
3255
+ this._content.update((store) => ({
3256
+ ...store,
3257
+ [namespace]: content,
3258
+ }));
3230
3259
  }
3231
- hasActions() {
3232
- return !!(this.props.showDownload || this.props.showCopy || this.props.showShare);
3260
+ /**
3261
+ * Registra múltiples namespaces de una vez
3262
+ *
3263
+ * @param contentStore Objeto con namespaces como keys
3264
+ */
3265
+ registerContentBulk(contentStore) {
3266
+ this._content.update((store) => ({
3267
+ ...store,
3268
+ ...contentStore,
3269
+ }));
3233
3270
  }
3234
- getDownloadLabel() {
3235
- return this.props.downloadLabel || 'Descargar';
3271
+ /**
3272
+ * Configura los idiomas soportados
3273
+ */
3274
+ setI18nLanguages(languages) {
3275
+ this._supportedLanguages.set(languages);
3236
3276
  }
3237
- getCopyLabel() {
3238
- return this.props.copyLabel || 'Copiar';
3277
+ /**
3278
+ * Obtiene todos los namespaces registrados
3279
+ */
3280
+ getNamespaces() {
3281
+ return Object.keys(this._content());
3239
3282
  }
3240
- getShareLabel() {
3241
- return this.props.shareLabel || 'Compartir';
3283
+ /**
3284
+ * Verifica si un namespace tiene traducciones
3285
+ */
3286
+ hasNamespace(namespace) {
3287
+ return namespace in this._content();
3242
3288
  }
3243
- async onDownload() {
3244
- try {
3245
- const options = this.props.downloadFilename
3246
- ? { filename: this.props.downloadFilename }
3247
- : undefined;
3248
- await this.qrService.download(this.props.qr, options);
3249
- this.actionComplete.emit({
3250
- action: 'download',
3251
- success: true,
3252
- qr: this.props.qr,
3289
+ /**
3290
+ * Carga idioma guardado en localStorage o detecta del navegador
3291
+ */
3292
+ loadStoredLanguage() {
3293
+ const stored = localStorage.getItem(LANG_STORAGE_KEY$1);
3294
+ if (stored && this.isValidLanguage(stored)) {
3295
+ this._lang.set(stored);
3296
+ return;
3297
+ }
3298
+ // Detectar idioma del navegador
3299
+ const browserLang = navigator.language.split('-')[0];
3300
+ if (this.isValidLanguage(browserLang)) {
3301
+ this._lang.set(browserLang);
3302
+ localStorage.setItem(LANG_STORAGE_KEY$1, browserLang);
3303
+ }
3304
+ }
3305
+ /**
3306
+ * Valida si un idioma está soportado
3307
+ */
3308
+ isValidLanguage(lang) {
3309
+ return this._supportedLanguages().includes(lang);
3310
+ }
3311
+ /**
3312
+ * Reemplaza {variable} en texto con valores de data
3313
+ *
3314
+ * @example
3315
+ * interpolate('Hola {name}', { name: 'Juan' }) // 'Hola Juan'
3316
+ */
3317
+ interpolate(text, data) {
3318
+ return Object.entries(data).reduce((result, [key, value]) => {
3319
+ const regex = new RegExp(`\\{${key}\\}`, 'g');
3320
+ return result.replace(regex, value);
3321
+ }, text);
3322
+ }
3323
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: I18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
3324
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: I18nService, providedIn: 'root' }); }
3325
+ }
3326
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: I18nService, decorators: [{
3327
+ type: Injectable,
3328
+ args: [{ providedIn: 'root' }]
3329
+ }], ctorParameters: () => [] });
3330
+
3331
+ /**
3332
+ * Pipe de traducción para templates.
3333
+ *
3334
+ * NOTA: Es impure para detectar cambios de idioma.
3335
+ * El costo es mínimo porque I18nService usa Signals internamente.
3336
+ *
3337
+ * @example
3338
+ * <!-- Busca en _global -->
3339
+ * {{ 'submit' | t }}
3340
+ *
3341
+ * <!-- Busca en namespace específico -->
3342
+ * {{ 'title' | t:'Login' }}
3343
+ *
3344
+ * <!-- Con interpolación -->
3345
+ * {{ 'welcome' | t:'Login':{ name: userName } }}
3346
+ *
3347
+ * <!-- En atributos -->
3348
+ * <val-button [label]="'submit' | t"></val-button>
3349
+ * <val-input [label]="'email' | t:'Login'"></val-input>
3350
+ */
3351
+ class TranslatePipe {
3352
+ constructor() {
3353
+ this.i18n = inject(I18nService);
3354
+ }
3355
+ /**
3356
+ * Transforma una key de traducción a su valor
3357
+ *
3358
+ * @param key Clave del texto
3359
+ * @param namespace Namespace opcional (default: '_global')
3360
+ * @param data Variables para interpolación opcional
3361
+ * @returns Texto traducido
3362
+ */
3363
+ transform(key, namespace, data) {
3364
+ return this.i18n.t(key, namespace, data);
3365
+ }
3366
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
3367
+ static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.14", ngImport: i0, type: TranslatePipe, isStandalone: true, name: "t", pure: false }); }
3368
+ }
3369
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TranslatePipe, decorators: [{
3370
+ type: Pipe,
3371
+ args: [{
3372
+ name: 't',
3373
+ standalone: true,
3374
+ pure: false, // Impure para detectar cambios de idioma
3375
+ }]
3376
+ }] });
3377
+
3378
+ /**
3379
+ * Traducciones por defecto de valtech-components.
3380
+ *
3381
+ * Estas traducciones se cargan automáticamente cuando se usa provideValtechI18n()
3382
+ * sin necesidad de configuración adicional.
3383
+ *
3384
+ * Las apps pueden sobrescribir cualquier key pasando su propio content:
3385
+ *
3386
+ * @example Sobrescribir traducciones específicas
3387
+ * ```typescript
3388
+ * provideValtechI18n({
3389
+ * content: {
3390
+ * _global: {
3391
+ * es: { success: '¡Lo hicimos!' }, // Sobrescribe solo esta key
3392
+ * en: { success: 'We did it!' },
3393
+ * },
3394
+ * },
3395
+ * })
3396
+ * ```
3397
+ *
3398
+ * @example Desactivar traducciones por defecto
3399
+ * ```typescript
3400
+ * provideValtechI18n({
3401
+ * includeDefaultContent: false,
3402
+ * content: MY_CUSTOM_CONTENT,
3403
+ * })
3404
+ * ```
3405
+ */
3406
+ const VALTECH_DEFAULT_CONTENT = {
3407
+ _global: {
3408
+ es: {
3409
+ // Acciones comunes
3410
+ submit: 'Enviar',
3411
+ cancel: 'Cancelar',
3412
+ save: 'Guardar',
3413
+ delete: 'Eliminar',
3414
+ edit: 'Editar',
3415
+ close: 'Cerrar',
3416
+ back: 'Volver',
3417
+ next: 'Siguiente',
3418
+ previous: 'Anterior',
3419
+ loading: 'Cargando...',
3420
+ search: 'Buscar',
3421
+ learnMore: 'Saber más',
3422
+ // Estados
3423
+ success: 'Éxito',
3424
+ error: 'Error',
3425
+ warning: 'Advertencia',
3426
+ info: 'Información',
3427
+ // Confirmaciones
3428
+ confirmDelete: '¿Estás seguro de que deseas eliminar?',
3429
+ confirmCancel: '¿Estás seguro de que deseas cancelar?',
3430
+ // Mensajes comunes
3431
+ noResults: 'No se encontraron resultados',
3432
+ required: 'Este campo es requerido',
3433
+ // Navegación
3434
+ home: 'Inicio',
3435
+ settings: 'Configuración',
3436
+ profile: 'Perfil',
3437
+ logout: 'Cerrar sesión',
3438
+ // Idiomas
3439
+ languageName_es: 'Español',
3440
+ languageName_en: 'English',
3441
+ // Componentes - Modales/Selects
3442
+ ok: 'Aceptar',
3443
+ selectOption: 'Seleccionar opción',
3444
+ selectPlaceholder: 'Seleccionar...',
3445
+ itemsSelected: 'elementos seleccionados',
3446
+ selectAll: 'Seleccionar todos',
3447
+ clear: 'Limpiar',
3448
+ apply: 'Aplicar',
3449
+ // Componentes - Fechas
3450
+ startDate: 'Fecha inicio',
3451
+ endDate: 'Fecha fin',
3452
+ day: 'día',
3453
+ days: 'días',
3454
+ // Componentes - Formularios
3455
+ uploadFile: 'Subir archivo',
3456
+ noFileSelected: 'No has seleccionado archivo',
3457
+ title: 'Título',
3458
+ description: 'Descripción',
3459
+ feedbackType: 'Tipo de feedback',
3460
+ feedbackSuccess: 'Feedback enviado exitosamente',
3461
+ feedbackError: 'Error al enviar el feedback',
3462
+ // Componentes - Búsqueda/Grid
3463
+ searchNumber: 'Buscar número...',
3464
+ selected: 'seleccionado/s',
3465
+ max: 'máx:',
3466
+ // Componentes - Listas
3467
+ loadMoreComments: 'Cargar más comentarios',
3468
+ noData: 'No hay datos',
3469
+ noRecordsFound: 'No se encontraron registros para mostrar.',
3470
+ // Componentes - Acciones
3471
+ copiedToClipboard: '¡Copiado al portapapeles!',
3472
+ language: 'Idioma',
3473
+ selectLanguage: 'Seleccionar idioma...',
3474
+ download: 'Descargar',
3475
+ copy: 'Copiar',
3476
+ share: 'Compartir',
3477
+ // Componentes - Data Table / Paginación
3478
+ actions: 'Acciones',
3479
+ showing: 'Mostrando',
3480
+ of: 'de',
3481
+ perPage: 'por página',
3482
+ firstPage: 'Primera página',
3483
+ previousPage: 'Página anterior',
3484
+ nextPage: 'Página siguiente',
3485
+ lastPage: 'Última página',
3486
+ // Componentes - Participant Card
3487
+ winner: 'Ganador',
3488
+ ticket: 'boleto',
3489
+ tickets: 'boletos',
3490
+ more: 'más',
3491
+ notes: 'Notas:',
3492
+ },
3493
+ en: {
3494
+ // Common actions
3495
+ submit: 'Submit',
3496
+ cancel: 'Cancel',
3497
+ save: 'Save',
3498
+ delete: 'Delete',
3499
+ edit: 'Edit',
3500
+ close: 'Close',
3501
+ back: 'Back',
3502
+ next: 'Next',
3503
+ previous: 'Previous',
3504
+ loading: 'Loading...',
3505
+ search: 'Search',
3506
+ learnMore: 'Learn more',
3507
+ // States
3508
+ success: 'Success',
3509
+ error: 'Error',
3510
+ warning: 'Warning',
3511
+ info: 'Information',
3512
+ // Confirmations
3513
+ confirmDelete: 'Are you sure you want to delete?',
3514
+ confirmCancel: 'Are you sure you want to cancel?',
3515
+ // Common messages
3516
+ noResults: 'No results found',
3517
+ required: 'This field is required',
3518
+ // Navigation
3519
+ home: 'Home',
3520
+ settings: 'Settings',
3521
+ profile: 'Profile',
3522
+ logout: 'Log out',
3523
+ // Languages
3524
+ languageName_es: 'Español',
3525
+ languageName_en: 'English',
3526
+ // Components - Modals/Selects
3527
+ ok: 'OK',
3528
+ selectOption: 'Select option',
3529
+ selectPlaceholder: 'Select...',
3530
+ itemsSelected: 'items selected',
3531
+ selectAll: 'Select all',
3532
+ clear: 'Clear',
3533
+ apply: 'Apply',
3534
+ // Components - Dates
3535
+ startDate: 'Start date',
3536
+ endDate: 'End date',
3537
+ day: 'day',
3538
+ days: 'days',
3539
+ // Components - Forms
3540
+ uploadFile: 'Upload file',
3541
+ noFileSelected: 'No file selected',
3542
+ title: 'Title',
3543
+ description: 'Description',
3544
+ feedbackType: 'Feedback type',
3545
+ feedbackSuccess: 'Feedback sent successfully',
3546
+ feedbackError: 'Error sending feedback',
3547
+ // Components - Search/Grid
3548
+ searchNumber: 'Search number...',
3549
+ selected: 'selected',
3550
+ max: 'max:',
3551
+ // Components - Lists
3552
+ loadMoreComments: 'Load more comments',
3553
+ noData: 'No data',
3554
+ noRecordsFound: 'No records found to display.',
3555
+ // Components - Actions
3556
+ copiedToClipboard: 'Copied to clipboard!',
3557
+ language: 'Language',
3558
+ selectLanguage: 'Select language...',
3559
+ download: 'Download',
3560
+ copy: 'Copy',
3561
+ share: 'Share',
3562
+ // Components - Data Table / Pagination
3563
+ actions: 'Actions',
3564
+ showing: 'Showing',
3565
+ of: 'of',
3566
+ perPage: 'per page',
3567
+ firstPage: 'First page',
3568
+ previousPage: 'Previous page',
3569
+ nextPage: 'Next page',
3570
+ lastPage: 'Last page',
3571
+ // Components - Participant Card
3572
+ winner: 'Winner',
3573
+ ticket: 'ticket',
3574
+ tickets: 'tickets',
3575
+ more: 'more',
3576
+ notes: 'Notes:',
3577
+ },
3578
+ },
3579
+ };
3580
+
3581
+ /**
3582
+ * Configura el sistema de internacionalización de Valtech Components.
3583
+ *
3584
+ * @param config Configuración de i18n
3585
+ * @returns Providers para agregar en app.config.ts
3586
+ *
3587
+ * @example
3588
+ * // app.config.ts
3589
+ * import { provideValtechI18n } from 'valtech-components';
3590
+ * import { GLOBAL_CONTENT } from './i18n/_global';
3591
+ * import { LOGIN_CONTENT } from './i18n/login.i18n';
3592
+ *
3593
+ * export const appConfig: ApplicationConfig = {
3594
+ * providers: [
3595
+ * provideValtechI18n({
3596
+ * defaultLanguage: 'es',
3597
+ * supportedLanguages: ['es', 'en'],
3598
+ * detectBrowserLanguage: true,
3599
+ * content: {
3600
+ * '_global': GLOBAL_CONTENT,
3601
+ * 'Login': LOGIN_CONTENT,
3602
+ * }
3603
+ * }),
3604
+ * ]
3605
+ * };
3606
+ */
3607
+ function provideValtechI18n(config = {}) {
3608
+ const mergedConfig = { ...DEFAULT_I18N_CONFIG, ...config };
3609
+ return makeEnvironmentProviders([
3610
+ {
3611
+ provide: APP_INITIALIZER,
3612
+ useFactory: (i18n) => {
3613
+ return () => {
3614
+ // Configurar idiomas soportados
3615
+ i18n.setI18nLanguages(mergedConfig.supportedLanguages);
3616
+ // Determinar contenido a registrar
3617
+ let contentToRegister = {};
3618
+ if (mergedConfig.includeDefaultContent !== false) {
3619
+ // Merge: defaults + user config (user gana)
3620
+ contentToRegister = deepMergeContent(VALTECH_DEFAULT_CONTENT, mergedConfig.content || {});
3621
+ }
3622
+ else if (mergedConfig.content) {
3623
+ // Solo contenido del usuario
3624
+ contentToRegister = mergedConfig.content;
3625
+ }
3626
+ // Registrar contenido
3627
+ if (Object.keys(contentToRegister).length > 0) {
3628
+ i18n.registerContentBulk(contentToRegister);
3629
+ }
3630
+ };
3631
+ },
3632
+ deps: [I18nService],
3633
+ multi: true,
3634
+ },
3635
+ ]);
3636
+ }
3637
+ /**
3638
+ * Deep merge de ContentStore.
3639
+ * El contenido de `override` sobrescribe keys específicas de `base`.
3640
+ *
3641
+ * @param base - Contenido base (defaults de valtech-components)
3642
+ * @param override - Contenido del usuario que sobrescribe
3643
+ * @returns ContentStore mergeado
3644
+ *
3645
+ * @example
3646
+ * const base = { _global: { es: { ok: 'Aceptar' } } };
3647
+ * const override = { _global: { es: { ok: 'Dale' } } };
3648
+ * // Resultado: { _global: { es: { ok: 'Dale' } } }
3649
+ */
3650
+ function deepMergeContent(base, override) {
3651
+ const result = {};
3652
+ // Copiar todos los namespaces del base
3653
+ for (const namespace of Object.keys(base)) {
3654
+ result[namespace] = deepMergeLanguagesContent(base[namespace], {});
3655
+ }
3656
+ // Mergear namespaces del override
3657
+ for (const namespace of Object.keys(override)) {
3658
+ if (result[namespace]) {
3659
+ // Namespace existe en base, hacer deep merge
3660
+ result[namespace] = deepMergeLanguagesContent(result[namespace], override[namespace]);
3661
+ }
3662
+ else {
3663
+ // Namespace nuevo, copiar completo
3664
+ result[namespace] = deepMergeLanguagesContent({}, override[namespace]);
3665
+ }
3666
+ }
3667
+ return result;
3668
+ }
3669
+ /**
3670
+ * Deep merge de LanguagesContent (un namespace).
3671
+ */
3672
+ function deepMergeLanguagesContent(base, override) {
3673
+ const result = {};
3674
+ const allLangs = new Set([
3675
+ ...Object.keys(base),
3676
+ ...Object.keys(override),
3677
+ ]);
3678
+ for (const lang of allLangs) {
3679
+ result[lang] = {
3680
+ ...(base[lang] || {}),
3681
+ ...(override[lang] || {}),
3682
+ };
3683
+ }
3684
+ return result;
3685
+ }
3686
+
3687
+ // Types
3688
+
3689
+ addIcons({ downloadOutline, copyOutline, shareOutline });
3690
+ /**
3691
+ * val-qr-code
3692
+ *
3693
+ * A component to display QR codes generated by QrGeneratorService.
3694
+ * Provides optional action buttons for download, copy, and share.
3695
+ *
3696
+ * @example Basic usage
3697
+ * ```typescript
3698
+ * qr = await this.qrService.generate({ data: 'https://example.com' });
3699
+ * ```
3700
+ * ```html
3701
+ * <val-qr-code [props]="{ qr: qr }"></val-qr-code>
3702
+ * ```
3703
+ *
3704
+ * @example With actions
3705
+ * ```html
3706
+ * <val-qr-code
3707
+ * [props]="{
3708
+ * qr: qr,
3709
+ * showDownload: true,
3710
+ * showCopy: true,
3711
+ * showShare: true,
3712
+ * displaySize: 200,
3713
+ * showBorder: true,
3714
+ * borderRadius: 12
3715
+ * }"
3716
+ * (actionComplete)="onAction($event)"
3717
+ * ></val-qr-code>
3718
+ * ```
3719
+ *
3720
+ * @input props: QrCodeMetadata - Configuration for the QR display
3721
+ * @output actionComplete - Emits when an action (download/copy/share) completes
3722
+ * @output imageLoad - Emits when the QR image loads
3723
+ * @output imageError - Emits when the QR image fails to load
3724
+ */
3725
+ class QrCodeComponent {
3726
+ constructor() {
3727
+ this.actionComplete = new EventEmitter();
3728
+ this.imageLoad = new EventEmitter();
3729
+ this.imageError = new EventEmitter();
3730
+ this.canShare = false;
3731
+ this.canCopy = false;
3732
+ this.qrService = inject(QrGeneratorService);
3733
+ this.i18n = inject(I18nService);
3734
+ }
3735
+ ngOnInit() {
3736
+ this.canShare = this.qrService.canShare();
3737
+ this.canCopy = this.qrService.canCopyToClipboard();
3738
+ }
3739
+ getDisplaySize() {
3740
+ if (this.props.displaySize) {
3741
+ return `${this.props.displaySize}px`;
3742
+ }
3743
+ return `${this.props.qr.config.width || 300}px`;
3744
+ }
3745
+ getBorderRadius() {
3746
+ return this.props.borderRadius ? `${this.props.borderRadius}px` : '0';
3747
+ }
3748
+ getPadding() {
3749
+ return this.props.padding ? `${this.props.padding}px` : '0';
3750
+ }
3751
+ getQrBorderRadius() {
3752
+ return this.props.qrBorderRadius ? `${this.props.qrBorderRadius}px` : '0';
3753
+ }
3754
+ getContainerClasses() {
3755
+ const classes = [];
3756
+ if (this.props.cssClass)
3757
+ classes.push(this.props.cssClass);
3758
+ if (this.props.showBorder)
3759
+ classes.push('with-border');
3760
+ if (this.props.loading)
3761
+ classes.push('loading');
3762
+ if (this.props.theme)
3763
+ classes.push(`theme--${this.props.theme}`);
3764
+ if (this.props.pulseOnHover)
3765
+ classes.push('pulse-on-hover');
3766
+ if (this.props.scaleOnHover)
3767
+ classes.push('scale-on-hover');
3768
+ if (this.props.gradient)
3769
+ classes.push('has-gradient');
3770
+ return classes.join(' ');
3771
+ }
3772
+ getContainerBackground() {
3773
+ if (this.props.gradient) {
3774
+ const { from, to, via, direction = 'to-bottom' } = this.props.gradient;
3775
+ const cssDirection = direction.replace(/-/g, ' ');
3776
+ if (via) {
3777
+ return `linear-gradient(${cssDirection}, ${from}, ${via}, ${to})`;
3778
+ }
3779
+ return `linear-gradient(${cssDirection}, ${from}, ${to})`;
3780
+ }
3781
+ return this.props.containerBackground || 'transparent';
3782
+ }
3783
+ getShadow() {
3784
+ const shadowMap = {
3785
+ none: 'none',
3786
+ sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
3787
+ md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
3788
+ lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
3789
+ xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
3790
+ };
3791
+ if (!this.props.shadow)
3792
+ return 'none';
3793
+ return shadowMap[this.props.shadow] || this.props.shadow;
3794
+ }
3795
+ getLabelFontSize() {
3796
+ return this.props.label?.fontSize ? `${this.props.label.fontSize}px` : '14px';
3797
+ }
3798
+ hasActions() {
3799
+ return !!(this.props.showDownload || this.props.showCopy || this.props.showShare);
3800
+ }
3801
+ getDownloadLabel() {
3802
+ return this.props.downloadLabel || this.i18n.t('download');
3803
+ }
3804
+ getCopyLabel() {
3805
+ return this.props.copyLabel || this.i18n.t('copy');
3806
+ }
3807
+ getShareLabel() {
3808
+ return this.props.shareLabel || this.i18n.t('share');
3809
+ }
3810
+ async onDownload() {
3811
+ try {
3812
+ const options = this.props.downloadFilename
3813
+ ? { filename: this.props.downloadFilename }
3814
+ : undefined;
3815
+ await this.qrService.download(this.props.qr, options);
3816
+ this.actionComplete.emit({
3817
+ action: 'download',
3818
+ success: true,
3819
+ qr: this.props.qr,
3253
3820
  });
3254
3821
  }
3255
3822
  catch (error) {
@@ -4896,7 +5463,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4896
5463
  * @input props: DateInputMetadata - Configuration for the date input (form control, hint, etc.)
4897
5464
  */
4898
5465
  class DateInputComponent {
4899
- constructor() { }
5466
+ /** Done button text - from props or i18n default */
5467
+ get doneText() {
5468
+ return this.props?.doneText || this.i18n.t('ok');
5469
+ }
5470
+ /** Cancel button text - from props or i18n default */
5471
+ get cancelText() {
5472
+ return this.props?.cancelText || this.i18n.t('cancel');
5473
+ }
5474
+ constructor() {
5475
+ this.i18n = inject(I18nService);
5476
+ }
4900
5477
  ngOnInit() {
4901
5478
  // Apply default values on initialization
4902
5479
  if (this.props?.withDefault || this.props?.value) {
@@ -4937,8 +5514,8 @@ class DateInputComponent {
4937
5514
  locale="es-ES"
4938
5515
  [firstDayOfWeek]="1"
4939
5516
  [showDefaultButtons]="true"
4940
- doneText="Aceptar"
4941
- cancelText="Cancelar"
5517
+ [doneText]="doneText"
5518
+ [cancelText]="cancelText"
4942
5519
  formatOptions="{
4943
5520
  date: { dateStyle: 'medium' },
4944
5521
  time: { timeStyle: 'short' }
@@ -4965,8 +5542,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
4965
5542
  locale="es-ES"
4966
5543
  [firstDayOfWeek]="1"
4967
5544
  [showDefaultButtons]="true"
4968
- doneText="Aceptar"
4969
- cancelText="Cancelar"
5545
+ [doneText]="doneText"
5546
+ [cancelText]="cancelText"
4970
5547
  formatOptions="{
4971
5548
  date: { dateStyle: 'medium' },
4972
5549
  time: { timeStyle: 'short' }
@@ -5105,502 +5682,209 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
5105
5682
  <div class="description-container" [class.expanded]="expanded" [class.has-gradient]="!expanded && isTruncated">
5106
5683
  <div class="content-wrapper">
5107
5684
  <ion-text>
5108
- <p class="description">
5109
- <span class="content">{{ expanded ? props.content : truncatedText }}</span>
5110
- </p>
5111
- </ion-text>
5112
- </div>
5113
- @if (!expanded && isTruncated) {
5114
- <span class="see-more" [style.color]="this.color()" (click)="toggleExpand()">
5115
- {{ getExpandText() }}
5116
- </span>
5117
- }
5118
- </div>
5119
- `, styles: [".description-container{position:relative;overflow:hidden;transition:max-height .3s ease-in-out}.description-container:not(.expanded){max-height:10rem}.description-container.expanded{max-height:none}.content-wrapper{overflow:hidden}.description-container:not(.expanded).has-gradient .content-wrapper{-webkit-mask-image:linear-gradient(to bottom,black 50%,transparent 100%);mask-image:linear-gradient(to bottom,black 50%,transparent 100%)}.description{margin:0}.see-more{display:block;font-weight:700;cursor:pointer;margin-top:.25rem}\n"] }]
5120
- }], propDecorators: { props: [{
5121
- type: Input
5122
- }] } });
5123
-
5124
- /**
5125
- * val-file-input
5126
- *
5127
- * A file input component for uploading files, integrated with Angular forms.
5128
- *
5129
- * @example
5130
- * <val-file-input [props]="{ control: myControl, accept: 'image/*' }"></val-file-input>
5131
- *
5132
- * @input props: FileInputMetadata - Configuration for the file input (form control, accept types, etc.)
5133
- */
5134
- class FileInputComponent {
5135
- constructor() {
5136
- this.contrastButton = {
5137
- ...PrimarySolidDefaultRoundButton('Subir archivo'),
5138
- color: 'light',
5139
- };
5140
- }
5141
- ngOnInit() { }
5142
- onFileSelected(event) {
5143
- this.selectedFile = event.target.files[0];
5144
- this.props.control.setValue(this.selectedFile);
5145
- }
5146
- reset() {
5147
- this.selectedFile = null;
5148
- this.fileInput.nativeElement.value = '';
5149
- }
5150
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5151
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileInputComponent, isStandalone: true, selector: "val-file-input", inputs: { props: "props" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: `
5152
- <div class="file-container">
5153
- <input style="display: none" type="file" (change)="onFileSelected($event)" #fileInput [accept]="props.accept" />
5154
- <div class="name-container">
5155
- <ion-icon [name]="selectedFile ? 'checkmark-circle-outline' : 'alert-circle-outline'"></ion-icon>
5156
- <val-text
5157
- style="margin-left: 4px;"
5158
- [props]="{
5159
- content: selectedFile ? selectedFile.name : (props.noFileText || 'No has seleccionado archivo'),
5160
- color: 'dark',
5161
- bold: false,
5162
- size: 'medium',
5163
- }"
5164
- ></val-text>
5165
- </div>
5166
- <val-button [props]="contrastButton" (onClick)="fileInput.click()"></val-button>
5167
- </div>
5168
- `, isInline: true, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.file-container{margin-top:.25rem}.name-container{display:flex;flex-direction:row;align-items:center}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
5169
- }
5170
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileInputComponent, decorators: [{
5171
- type: Component,
5172
- args: [{ selector: 'val-file-input', standalone: true, imports: [IonIcon, TextComponent, ButtonComponent], template: `
5173
- <div class="file-container">
5174
- <input style="display: none" type="file" (change)="onFileSelected($event)" #fileInput [accept]="props.accept" />
5175
- <div class="name-container">
5176
- <ion-icon [name]="selectedFile ? 'checkmark-circle-outline' : 'alert-circle-outline'"></ion-icon>
5177
- <val-text
5178
- style="margin-left: 4px;"
5179
- [props]="{
5180
- content: selectedFile ? selectedFile.name : (props.noFileText || 'No has seleccionado archivo'),
5181
- color: 'dark',
5182
- bold: false,
5183
- size: 'medium',
5184
- }"
5185
- ></val-text>
5186
- </div>
5187
- <val-button [props]="contrastButton" (onClick)="fileInput.click()"></val-button>
5188
- </div>
5189
- `, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.file-container{margin-top:.25rem}.name-container{display:flex;flex-direction:row;align-items:center}\n"] }]
5190
- }], ctorParameters: () => [], propDecorators: { props: [{
5191
- type: Input
5192
- }], fileInput: [{
5193
- type: ViewChild,
5194
- args: ['fileInput']
5195
- }] } });
5196
-
5197
- /**
5198
- * val-hint
5199
- *
5200
- * Displays validation error messages for a form input, using Angular forms.
5201
- *
5202
- * @example
5203
- * <val-hint [props]="{ control: myControl, errors: { required: 'Required field' } }"></val-hint>
5204
- *
5205
- * @input props: InputMetadata - Configuration for the input (form control, errors, etc.)
5206
- */
5207
- class HintComponent {
5208
- constructor() { }
5209
- ngOnInit() { }
5210
- get shouldShowErrors() {
5211
- if (this.props.control) {
5212
- // Normal field with single control
5213
- return this.props.control.invalid && (this.props.control.touched || this.props.control.dirty);
5214
- }
5215
- else if (this.props.fromControl && this.props.toControl) {
5216
- // NUMBER_FROM_TO field with separate controls
5217
- const fromInvalid = this.props.fromControl.invalid && (this.props.fromControl.touched || this.props.fromControl.dirty);
5218
- const toInvalid = this.props.toControl.invalid && (this.props.toControl.touched || this.props.toControl.dirty);
5219
- return fromInvalid || toInvalid;
5220
- }
5221
- return false;
5222
- }
5223
- get Errors() {
5224
- const keys = Object.keys(this.props.errors || {});
5225
- const errors = [];
5226
- keys.map((e) => {
5227
- if (this.props.control && this.props.control.hasError(e)) {
5228
- // Normal field
5229
- errors.push(this.props.errors[e]);
5230
- }
5231
- else if (this.props.fromControl && this.props.toControl) {
5232
- // NUMBER_FROM_TO field - check both controls
5233
- const fromLabel = this.props.fromLabel || 'Inicial';
5234
- const toLabel = this.props.toLabel || 'Final';
5235
- if (this.props.fromControl.hasError(e)) {
5236
- errors.push(`${fromLabel}: ${this.props.errors[e]}`);
5237
- }
5238
- if (this.props.toControl.hasError(e)) {
5239
- errors.push(`${toLabel}: ${this.props.errors[e]}`);
5240
- }
5241
- }
5242
- });
5243
- return errors;
5244
- }
5245
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HintComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5246
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: HintComponent, isStandalone: true, selector: "val-hint", inputs: { props: "props" }, ngImport: i0, template: `
5247
- <div class="hint-container" *ngIf="shouldShowErrors">
5248
- <val-text
5249
- *ngFor="let e of Errors"
5250
- [props]="{
5251
- content: e,
5252
- color: 'danger',
5253
- bold: false,
5254
- size: 'small',
5255
- }"
5256
- ></val-text>
5257
- </div>
5258
- `, isInline: true, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.hint-container{margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
5259
- }
5260
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HintComponent, decorators: [{
5261
- type: Component,
5262
- args: [{ selector: 'val-hint', standalone: true, imports: [CommonModule, TextComponent], template: `
5263
- <div class="hint-container" *ngIf="shouldShowErrors">
5264
- <val-text
5265
- *ngFor="let e of Errors"
5266
- [props]="{
5267
- content: e,
5268
- color: 'danger',
5269
- bold: false,
5270
- size: 'small',
5271
- }"
5272
- ></val-text>
5273
- </div>
5274
- `, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.hint-container{margin-top:.25rem}\n"] }]
5275
- }], ctorParameters: () => [], propDecorators: { props: [{
5276
- type: Input
5277
- }] } });
5278
-
5279
- /**
5280
- * val-hour-input
5281
- *
5282
- * A time picker input integrated with Angular forms, using Ionic's datetime component.
5283
- *
5284
- * @example
5285
- * <val-hour-input [props]="{ control: myControl }"></val-hour-input>
5286
- *
5287
- * @input props: InputMetadata - Configuration for the time input (form control, etc.)
5288
- */
5289
- class HourInputComponent {
5290
- constructor() { }
5291
- ngOnInit() { }
5292
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HourInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5293
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: HourInputComponent, isStandalone: true, selector: "val-hour-input", inputs: { props: "props" }, ngImport: i0, template: ` <ion-datetime [formControl]="props.control" presentation="time"></ion-datetime>`, isInline: true, styles: [""], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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"] }] }); }
5294
- }
5295
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HourInputComponent, decorators: [{
5296
- type: Component,
5297
- args: [{ selector: 'val-hour-input', standalone: true, imports: [ReactiveFormsModule, IonDatetime], template: ` <ion-datetime [formControl]="props.control" presentation="time"></ion-datetime>` }]
5298
- }], ctorParameters: () => [], propDecorators: { props: [{
5685
+ <p class="description">
5686
+ <span class="content">{{ expanded ? props.content : truncatedText }}</span>
5687
+ </p>
5688
+ </ion-text>
5689
+ </div>
5690
+ @if (!expanded && isTruncated) {
5691
+ <span class="see-more" [style.color]="this.color()" (click)="toggleExpand()">
5692
+ {{ getExpandText() }}
5693
+ </span>
5694
+ }
5695
+ </div>
5696
+ `, styles: [".description-container{position:relative;overflow:hidden;transition:max-height .3s ease-in-out}.description-container:not(.expanded){max-height:10rem}.description-container.expanded{max-height:none}.content-wrapper{overflow:hidden}.description-container:not(.expanded).has-gradient .content-wrapper{-webkit-mask-image:linear-gradient(to bottom,black 50%,transparent 100%);mask-image:linear-gradient(to bottom,black 50%,transparent 100%)}.description{margin:0}.see-more{display:block;font-weight:700;cursor:pointer;margin-top:.25rem}\n"] }]
5697
+ }], propDecorators: { props: [{
5299
5698
  type: Input
5300
5699
  }] } });
5301
5700
 
5302
5701
  /**
5303
- * Valores por defecto de configuración
5304
- */
5305
- const DEFAULT_I18N_CONFIG = {
5306
- defaultLanguage: 'es',
5307
- supportedLanguages: ['es', 'en'],
5308
- detectBrowserLanguage: true,
5309
- content: {},
5310
- };
5311
- /**
5312
- * Clave para persistir idioma en localStorage
5313
- */
5314
- const LANG_STORAGE_KEY$1 = 'app_lang';
5315
-
5316
- /**
5317
- * Servicio de internacionalización basado en Angular Signals.
5702
+ * val-file-input
5318
5703
  *
5319
- * Características:
5320
- * - Sin RxJS: usa Signals para evitar memory leaks y congelamiento
5321
- * - Namespace-based: organiza traducciones por contexto
5322
- * - Fallback multi-nivel: namespace → _global → placeholder
5323
- * - Interpolación: soporta {variable} en textos
5704
+ * A file input component for uploading files, integrated with Angular forms.
5324
5705
  *
5325
5706
  * @example
5326
- * // En un componente
5327
- * i18n = inject(I18nService);
5328
- *
5329
- * // Obtener texto
5330
- * const title = this.i18n.t('title', 'Login');
5331
- *
5332
- * // Con interpolación
5333
- * const welcome = this.i18n.t('welcome', 'Login', { name: 'Juan' });
5707
+ * <val-file-input [props]="{ control: myControl, accept: 'image/*' }"></val-file-input>
5334
5708
  *
5335
- * // Cambiar idioma
5336
- * this.i18n.setLanguage('en');
5709
+ * @input props: FileInputMetadata - Configuration for the file input (form control, accept types, etc.)
5337
5710
  */
5338
- class I18nService {
5711
+ class FileInputComponent {
5339
5712
  constructor() {
5340
- // Estado interno con Signals
5341
- this._lang = signal(DEFAULT_I18N_CONFIG.defaultLanguage);
5342
- this._content = signal({});
5343
- this._supportedLanguages = signal(DEFAULT_I18N_CONFIG.supportedLanguages);
5344
- // Públicos readonly
5345
- this.lang = this._lang.asReadonly();
5346
- this.supportedLanguages = this._supportedLanguages.asReadonly();
5347
- // Computed para verificaciones rápidas
5348
- this.isSpanish = computed(() => this._lang() === 'es');
5349
- this.isEnglish = computed(() => this._lang() === 'en');
5350
- this.loadStoredLanguage();
5351
- }
5352
- /**
5353
- * Obtiene texto traducido (alias corto de getText)
5354
- *
5355
- * @param key Clave del texto
5356
- * @param namespace Namespace (default: '_global')
5357
- * @param data Variables para interpolación
5358
- * @returns Texto traducido o placeholder [namespace.key]
5359
- *
5360
- * @example
5361
- * i18n.t('submit'); // busca en _global
5362
- * i18n.t('title', 'Login'); // busca en Login
5363
- * i18n.t('welcome', 'Login', {name}); // con interpolación
5364
- */
5365
- t(key, namespace, data) {
5366
- return this.getText(key, namespace, data);
5367
- }
5368
- /**
5369
- * Obtiene texto traducido
5370
- *
5371
- * Fallback order:
5372
- * 1. content[namespace][lang][key]
5373
- * 2. content['_global'][lang][key]
5374
- * 3. "[namespace.key]" (placeholder)
5375
- */
5376
- getText(key, namespace, data) {
5377
- const content = this._content();
5378
- const lang = this._lang();
5379
- const ns = namespace || '_global';
5380
- // Buscar en namespace específico
5381
- let text = content[ns]?.[lang]?.[key];
5382
- // Fallback a _global
5383
- if (!text && ns !== '_global') {
5384
- text = content['_global']?.[lang]?.[key];
5385
- }
5386
- // Fallback a placeholder
5387
- if (!text) {
5388
- console.warn(`[i18n] Missing translation: ${ns}.${key} (${lang})`);
5389
- return `[${ns}.${key}]`;
5390
- }
5391
- // Aplicar interpolación si hay data
5392
- if (data) {
5393
- return this.interpolate(text, data);
5394
- }
5395
- return text;
5396
- }
5397
- /**
5398
- * Cambia el idioma de la aplicación
5399
- *
5400
- * @param lang Nuevo idioma
5401
- * @param forceReload Si true, recarga la página (fallback si reactividad falla)
5402
- */
5403
- setLanguage(lang, forceReload = false) {
5404
- if (!this._supportedLanguages().includes(lang)) {
5405
- console.warn(`[i18n] Language '${lang}' not in supported languages`);
5406
- return;
5407
- }
5408
- if (lang === this._lang()) {
5409
- return;
5410
- }
5411
- // Persistir en localStorage
5412
- localStorage.setItem(LANG_STORAGE_KEY$1, lang);
5413
- // Actualizar signal
5414
- this._lang.set(lang);
5415
- // Fallback: recargar si se solicita
5416
- if (forceReload) {
5417
- window.location.reload();
5418
- }
5419
- }
5420
- /**
5421
- * Registra contenido de traducciones para un namespace
5422
- *
5423
- * @param namespace Nombre del namespace
5424
- * @param content Contenido de traducciones
5425
- *
5426
- * @example
5427
- * i18n.registerContent('Login', {
5428
- * es: { title: 'Iniciar sesión' },
5429
- * en: { title: 'Sign in' }
5430
- * });
5431
- */
5432
- registerContent(namespace, content) {
5433
- this._content.update((store) => ({
5434
- ...store,
5435
- [namespace]: content,
5436
- }));
5437
- }
5438
- /**
5439
- * Registra múltiples namespaces de una vez
5440
- *
5441
- * @param contentStore Objeto con namespaces como keys
5442
- */
5443
- registerContentBulk(contentStore) {
5444
- this._content.update((store) => ({
5445
- ...store,
5446
- ...contentStore,
5447
- }));
5448
- }
5449
- /**
5450
- * Configura los idiomas soportados
5451
- */
5452
- setI18nLanguages(languages) {
5453
- this._supportedLanguages.set(languages);
5454
- }
5455
- /**
5456
- * Obtiene todos los namespaces registrados
5457
- */
5458
- getNamespaces() {
5459
- return Object.keys(this._content());
5713
+ this.i18n = inject(I18nService);
5460
5714
  }
5461
- /**
5462
- * Verifica si un namespace tiene traducciones
5463
- */
5464
- hasNamespace(namespace) {
5465
- return namespace in this._content();
5715
+ ngOnInit() {
5716
+ // Initialize button with i18n text
5717
+ this.contrastButton = {
5718
+ ...PrimarySolidDefaultRoundButton(this.props?.buttonText || this.i18n.t('uploadFile')),
5719
+ color: 'light',
5720
+ };
5466
5721
  }
5467
- /**
5468
- * Carga idioma guardado en localStorage o detecta del navegador
5469
- */
5470
- loadStoredLanguage() {
5471
- const stored = localStorage.getItem(LANG_STORAGE_KEY$1);
5472
- if (stored && this.isValidLanguage(stored)) {
5473
- this._lang.set(stored);
5474
- return;
5475
- }
5476
- // Detectar idioma del navegador
5477
- const browserLang = navigator.language.split('-')[0];
5478
- if (this.isValidLanguage(browserLang)) {
5479
- this._lang.set(browserLang);
5480
- localStorage.setItem(LANG_STORAGE_KEY$1, browserLang);
5722
+ /** Get display text for file status */
5723
+ getFileDisplayText() {
5724
+ if (this.selectedFile) {
5725
+ return this.selectedFile.name;
5481
5726
  }
5727
+ return this.props?.noFileText || this.i18n.t('noFileSelected');
5482
5728
  }
5483
- /**
5484
- * Valida si un idioma está soportado
5485
- */
5486
- isValidLanguage(lang) {
5487
- return this._supportedLanguages().includes(lang);
5729
+ onFileSelected(event) {
5730
+ this.selectedFile = event.target.files[0];
5731
+ this.props.control.setValue(this.selectedFile);
5488
5732
  }
5489
- /**
5490
- * Reemplaza {variable} en texto con valores de data
5491
- *
5492
- * @example
5493
- * interpolate('Hola {name}', { name: 'Juan' }) // 'Hola Juan'
5494
- */
5495
- interpolate(text, data) {
5496
- return Object.entries(data).reduce((result, [key, value]) => {
5497
- const regex = new RegExp(`\\{${key}\\}`, 'g');
5498
- return result.replace(regex, value);
5499
- }, text);
5733
+ reset() {
5734
+ this.selectedFile = null;
5735
+ this.fileInput.nativeElement.value = '';
5500
5736
  }
5501
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: I18nService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
5502
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: I18nService, providedIn: 'root' }); }
5737
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5738
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: FileInputComponent, isStandalone: true, selector: "val-file-input", inputs: { props: "props" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: `
5739
+ <div class="file-container">
5740
+ <input style="display: none" type="file" (change)="onFileSelected($event)" #fileInput [accept]="props.accept" />
5741
+ <div class="name-container">
5742
+ <ion-icon [name]="selectedFile ? 'checkmark-circle-outline' : 'alert-circle-outline'"></ion-icon>
5743
+ <val-text
5744
+ style="margin-left: 4px;"
5745
+ [props]="{
5746
+ content: getFileDisplayText(),
5747
+ color: 'dark',
5748
+ bold: false,
5749
+ size: 'medium',
5750
+ }"
5751
+ ></val-text>
5752
+ </div>
5753
+ <val-button [props]="contrastButton" (onClick)="fileInput.click()"></val-button>
5754
+ </div>
5755
+ `, isInline: true, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.file-container{margin-top:.25rem}.name-container{display:flex;flex-direction:row;align-items:center}\n"], dependencies: [{ kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }, { kind: "component", type: ButtonComponent, selector: "val-button", inputs: ["preset", "props"], outputs: ["onClick"] }] }); }
5503
5756
  }
5504
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: I18nService, decorators: [{
5505
- type: Injectable,
5506
- args: [{ providedIn: 'root' }]
5507
- }], ctorParameters: () => [] });
5757
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FileInputComponent, decorators: [{
5758
+ type: Component,
5759
+ args: [{ selector: 'val-file-input', standalone: true, imports: [IonIcon, TextComponent, ButtonComponent], template: `
5760
+ <div class="file-container">
5761
+ <input style="display: none" type="file" (change)="onFileSelected($event)" #fileInput [accept]="props.accept" />
5762
+ <div class="name-container">
5763
+ <ion-icon [name]="selectedFile ? 'checkmark-circle-outline' : 'alert-circle-outline'"></ion-icon>
5764
+ <val-text
5765
+ style="margin-left: 4px;"
5766
+ [props]="{
5767
+ content: getFileDisplayText(),
5768
+ color: 'dark',
5769
+ bold: false,
5770
+ size: 'medium',
5771
+ }"
5772
+ ></val-text>
5773
+ </div>
5774
+ <val-button [props]="contrastButton" (onClick)="fileInput.click()"></val-button>
5775
+ </div>
5776
+ `, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.file-container{margin-top:.25rem}.name-container{display:flex;flex-direction:row;align-items:center}\n"] }]
5777
+ }], ctorParameters: () => [], propDecorators: { props: [{
5778
+ type: Input
5779
+ }], fileInput: [{
5780
+ type: ViewChild,
5781
+ args: ['fileInput']
5782
+ }] } });
5508
5783
 
5509
5784
  /**
5510
- * Pipe de traducción para templates.
5785
+ * val-hint
5511
5786
  *
5512
- * NOTA: Es impure para detectar cambios de idioma.
5513
- * El costo es mínimo porque I18nService usa Signals internamente.
5787
+ * Displays validation error messages for a form input, using Angular forms.
5514
5788
  *
5515
5789
  * @example
5516
- * <!-- Busca en _global -->
5517
- * {{ 'submit' | t }}
5518
- *
5519
- * <!-- Busca en namespace específico -->
5520
- * {{ 'title' | t:'Login' }}
5521
- *
5522
- * <!-- Con interpolación -->
5523
- * {{ 'welcome' | t:'Login':{ name: userName } }}
5790
+ * <val-hint [props]="{ control: myControl, errors: { required: 'Required field' } }"></val-hint>
5524
5791
  *
5525
- * <!-- En atributos -->
5526
- * <val-button [label]="'submit' | t"></val-button>
5527
- * <val-input [label]="'email' | t:'Login'"></val-input>
5792
+ * @input props: InputMetadata - Configuration for the input (form control, errors, etc.)
5528
5793
  */
5529
- class TranslatePipe {
5530
- constructor() {
5531
- this.i18n = inject(I18nService);
5794
+ class HintComponent {
5795
+ constructor() { }
5796
+ ngOnInit() { }
5797
+ get shouldShowErrors() {
5798
+ if (this.props.control) {
5799
+ // Normal field with single control
5800
+ return this.props.control.invalid && (this.props.control.touched || this.props.control.dirty);
5801
+ }
5802
+ else if (this.props.fromControl && this.props.toControl) {
5803
+ // NUMBER_FROM_TO field with separate controls
5804
+ const fromInvalid = this.props.fromControl.invalid && (this.props.fromControl.touched || this.props.fromControl.dirty);
5805
+ const toInvalid = this.props.toControl.invalid && (this.props.toControl.touched || this.props.toControl.dirty);
5806
+ return fromInvalid || toInvalid;
5807
+ }
5808
+ return false;
5532
5809
  }
5533
- /**
5534
- * Transforma una key de traducción a su valor
5535
- *
5536
- * @param key Clave del texto
5537
- * @param namespace Namespace opcional (default: '_global')
5538
- * @param data Variables para interpolación opcional
5539
- * @returns Texto traducido
5540
- */
5541
- transform(key, namespace, data) {
5542
- return this.i18n.t(key, namespace, data);
5810
+ get Errors() {
5811
+ const keys = Object.keys(this.props.errors || {});
5812
+ const errors = [];
5813
+ keys.map((e) => {
5814
+ if (this.props.control && this.props.control.hasError(e)) {
5815
+ // Normal field
5816
+ errors.push(this.props.errors[e]);
5817
+ }
5818
+ else if (this.props.fromControl && this.props.toControl) {
5819
+ // NUMBER_FROM_TO field - check both controls
5820
+ const fromLabel = this.props.fromLabel || 'Inicial';
5821
+ const toLabel = this.props.toLabel || 'Final';
5822
+ if (this.props.fromControl.hasError(e)) {
5823
+ errors.push(`${fromLabel}: ${this.props.errors[e]}`);
5824
+ }
5825
+ if (this.props.toControl.hasError(e)) {
5826
+ errors.push(`${toLabel}: ${this.props.errors[e]}`);
5827
+ }
5828
+ }
5829
+ });
5830
+ return errors;
5543
5831
  }
5544
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TranslatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
5545
- static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "18.2.14", ngImport: i0, type: TranslatePipe, isStandalone: true, name: "t", pure: false }); }
5832
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HintComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5833
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: HintComponent, isStandalone: true, selector: "val-hint", inputs: { props: "props" }, ngImport: i0, template: `
5834
+ <div class="hint-container" *ngIf="shouldShowErrors">
5835
+ <val-text
5836
+ *ngFor="let e of Errors"
5837
+ [props]="{
5838
+ content: e,
5839
+ color: 'danger',
5840
+ bold: false,
5841
+ size: 'small',
5842
+ }"
5843
+ ></val-text>
5844
+ </div>
5845
+ `, isInline: true, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.hint-container{margin-top:.25rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TextComponent, selector: "val-text", inputs: ["props"] }] }); }
5546
5846
  }
5547
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: TranslatePipe, decorators: [{
5548
- type: Pipe,
5549
- args: [{
5550
- name: 't',
5551
- standalone: true,
5552
- pure: false, // Impure para detectar cambios de idioma
5553
- }]
5554
- }] });
5847
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HintComponent, decorators: [{
5848
+ type: Component,
5849
+ args: [{ selector: 'val-hint', standalone: true, imports: [CommonModule, TextComponent], template: `
5850
+ <div class="hint-container" *ngIf="shouldShowErrors">
5851
+ <val-text
5852
+ *ngFor="let e of Errors"
5853
+ [props]="{
5854
+ content: e,
5855
+ color: 'danger',
5856
+ bold: false,
5857
+ size: 'small',
5858
+ }"
5859
+ ></val-text>
5860
+ </div>
5861
+ `, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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:.5rem 0}@media (min-width: 768px){.input{margin:.75rem 0}}.hint-container{margin-top:.25rem}\n"] }]
5862
+ }], ctorParameters: () => [], propDecorators: { props: [{
5863
+ type: Input
5864
+ }] } });
5555
5865
 
5556
5866
  /**
5557
- * Configura el sistema de internacionalización de Valtech Components.
5867
+ * val-hour-input
5558
5868
  *
5559
- * @param config Configuración de i18n
5560
- * @returns Providers para agregar en app.config.ts
5869
+ * A time picker input integrated with Angular forms, using Ionic's datetime component.
5561
5870
  *
5562
5871
  * @example
5563
- * // app.config.ts
5564
- * import { provideValtechI18n } from 'valtech-components';
5565
- * import { GLOBAL_CONTENT } from './i18n/_global';
5566
- * import { LOGIN_CONTENT } from './i18n/login.i18n';
5872
+ * <val-hour-input [props]="{ control: myControl }"></val-hour-input>
5567
5873
  *
5568
- * export const appConfig: ApplicationConfig = {
5569
- * providers: [
5570
- * provideValtechI18n({
5571
- * defaultLanguage: 'es',
5572
- * supportedLanguages: ['es', 'en'],
5573
- * detectBrowserLanguage: true,
5574
- * content: {
5575
- * '_global': GLOBAL_CONTENT,
5576
- * 'Login': LOGIN_CONTENT,
5577
- * }
5578
- * }),
5579
- * ]
5580
- * };
5874
+ * @input props: InputMetadata - Configuration for the time input (form control, etc.)
5581
5875
  */
5582
- function provideValtechI18n(config = {}) {
5583
- const mergedConfig = { ...DEFAULT_I18N_CONFIG, ...config };
5584
- return makeEnvironmentProviders([
5585
- {
5586
- provide: APP_INITIALIZER,
5587
- useFactory: (i18n) => {
5588
- return () => {
5589
- // Configurar idiomas soportados
5590
- i18n.setI18nLanguages(mergedConfig.supportedLanguages);
5591
- // Registrar contenido inicial
5592
- if (mergedConfig.content && Object.keys(mergedConfig.content).length > 0) {
5593
- i18n.registerContentBulk(mergedConfig.content);
5594
- }
5595
- };
5596
- },
5597
- deps: [I18nService],
5598
- multi: true,
5599
- },
5600
- ]);
5876
+ class HourInputComponent {
5877
+ constructor() { }
5878
+ ngOnInit() { }
5879
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HourInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
5880
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: HourInputComponent, isStandalone: true, selector: "val-hour-input", inputs: { props: "props" }, ngImport: i0, template: ` <ion-datetime [formControl]="props.control" presentation="time"></ion-datetime>`, isInline: true, styles: [""], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { 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"] }] }); }
5601
5881
  }
5602
-
5603
- // Types
5882
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: HourInputComponent, decorators: [{
5883
+ type: Component,
5884
+ args: [{ selector: 'val-hour-input', standalone: true, imports: [ReactiveFormsModule, IonDatetime], template: ` <ion-datetime [formControl]="props.control" presentation="time"></ion-datetime>` }]
5885
+ }], ctorParameters: () => [], propDecorators: { props: [{
5886
+ type: Input
5887
+ }] } });
5604
5888
 
5605
5889
  /**
5606
5890
  * val-popover-selector
@@ -5640,6 +5924,7 @@ class PopoverSelectorComponent {
5640
5924
  * Emits the selected value(s).
5641
5925
  */
5642
5926
  this.selectionChange = new EventEmitter();
5927
+ this.i18n = inject(I18nService);
5643
5928
  // Register required icons
5644
5929
  addIcons({ chevronDown });
5645
5930
  }
@@ -5647,19 +5932,19 @@ class PopoverSelectorComponent {
5647
5932
  * Get placeholder text.
5648
5933
  */
5649
5934
  getPlaceholderText() {
5650
- return this.props.placeholder || 'Seleccionar...';
5935
+ return this.props.placeholder || this.i18n.t('selectPlaceholder');
5651
5936
  }
5652
5937
  /**
5653
5938
  * Get cancel text.
5654
5939
  */
5655
5940
  getCancelText() {
5656
- return this.props.cancelText || 'Cancelar';
5941
+ return this.props.cancelText || this.i18n.t('cancel');
5657
5942
  }
5658
5943
  /**
5659
5944
  * Get ok text.
5660
5945
  */
5661
5946
  getOkText() {
5662
- return this.props.okText || 'Aceptar';
5947
+ return this.props.okText || this.i18n.t('ok');
5663
5948
  }
5664
5949
  /**
5665
5950
  * Handle selection change from the ion-select.
@@ -5687,7 +5972,7 @@ class PopoverSelectorComponent {
5687
5972
  const option = this.props.options.find(opt => opt.value === this.props.selectedValue[0]);
5688
5973
  return option?.label || this.props.selectedValue[0];
5689
5974
  }
5690
- return `${this.props.selectedValue.length} seleccionados`;
5975
+ return `${this.props.selectedValue.length} ${this.i18n.t('selected')}`;
5691
5976
  }
5692
5977
  // Single selection
5693
5978
  const selectedOption = this.props.options.find(opt => opt.value === this.props.selectedValue);
@@ -5854,9 +6139,9 @@ class LanguageSelectorComponent {
5854
6139
  this.popoverProps = {
5855
6140
  options,
5856
6141
  selectedValue: currentLanguage,
5857
- label: this.props.showLabel !== false ? (this.props.label || 'Idioma') : undefined,
6142
+ label: this.props.showLabel !== false ? (this.props.label || this.i18n.t('language')) : undefined,
5858
6143
  icon: 'language',
5859
- placeholder: 'Seleccionar idioma...',
6144
+ placeholder: this.i18n.t('selectLanguage'),
5860
6145
  color: this.props.color || 'medium',
5861
6146
  size: this.props.size || 'default',
5862
6147
  fill: this.props.fill || 'outline',
@@ -5866,8 +6151,8 @@ class LanguageSelectorComponent {
5866
6151
  interface: 'popover',
5867
6152
  showCheckmark: true,
5868
6153
  multiple: false,
5869
- cancelText: 'Cancelar',
5870
- okText: 'Aceptar',
6154
+ cancelText: this.i18n.t('cancel'),
6155
+ okText: this.i18n.t('ok'),
5871
6156
  };
5872
6157
  }
5873
6158
  /** Get display name for a language code (public for template access) */
@@ -7285,6 +7570,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7285
7570
  * @output blurEvent - Emits when the searchbar loses focus.
7286
7571
  */
7287
7572
  class SearchbarComponent {
7573
+ /** Get placeholder text */
7574
+ getPlaceholder() {
7575
+ return this.placeholder || this.i18n.t('search');
7576
+ }
7577
+ /** Get cancel button text */
7578
+ getCancelText() {
7579
+ return this.cancelText || this.i18n.t('cancel');
7580
+ }
7288
7581
  constructor() {
7289
7582
  /**
7290
7583
  * Emits the search term on input.
@@ -7298,6 +7591,7 @@ class SearchbarComponent {
7298
7591
  * Emits when the searchbar loses focus.
7299
7592
  */
7300
7593
  this.blurEvent = new EventEmitter();
7594
+ this.i18n = inject(I18nService);
7301
7595
  }
7302
7596
  onSearch($event) {
7303
7597
  const searchTerm = $event.detail.value;
@@ -7310,14 +7604,14 @@ class SearchbarComponent {
7310
7604
  this.blurEvent.emit();
7311
7605
  }
7312
7606
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SearchbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7313
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SearchbarComponent, isStandalone: true, selector: "val-searchbar", inputs: { disabled: "disabled" }, outputs: { filterEvent: "filterEvent", focusEvent: "focusEvent", blurEvent: "blurEvent" }, ngImport: i0, template: `
7607
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: SearchbarComponent, isStandalone: true, selector: "val-searchbar", inputs: { disabled: "disabled", placeholder: "placeholder", cancelText: "cancelText" }, outputs: { filterEvent: "filterEvent", focusEvent: "focusEvent", blurEvent: "blurEvent" }, ngImport: i0, template: `
7314
7608
  <ion-searchbar
7315
7609
  mode="ios"
7316
7610
  debounce="500"
7317
- placeholder="Búsqueda"
7611
+ [placeholder]="getPlaceholder()"
7318
7612
  [disabled]="disabled"
7319
7613
  showCancelButton="focus"
7320
- cancelButtonText="Cancelar"
7614
+ [cancelButtonText]="getCancelText()"
7321
7615
  (ionInput)="onSearch($event)"
7322
7616
  (ionBlur)="onBlur()"
7323
7617
  (ionFocus)="onFocus()"
@@ -7331,10 +7625,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7331
7625
  <ion-searchbar
7332
7626
  mode="ios"
7333
7627
  debounce="500"
7334
- placeholder="Búsqueda"
7628
+ [placeholder]="getPlaceholder()"
7335
7629
  [disabled]="disabled"
7336
7630
  showCancelButton="focus"
7337
- cancelButtonText="Cancelar"
7631
+ [cancelButtonText]="getCancelText()"
7338
7632
  (ionInput)="onSearch($event)"
7339
7633
  (ionBlur)="onBlur()"
7340
7634
  (ionFocus)="onFocus()"
@@ -7343,6 +7637,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7343
7637
  `, styles: [":root{--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)}@media (prefers-color-scheme: dark){:root{--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)}ion-searchbar{--cancel-button-color: var(--ion-color-dark);--background: var(--ion-color-light);font-family:var(--ion-default-font),Arial,sans-serif}\n"] }]
7344
7638
  }], ctorParameters: () => [], propDecorators: { disabled: [{
7345
7639
  type: Input
7640
+ }], placeholder: [{
7641
+ type: Input
7642
+ }], cancelText: [{
7643
+ type: Input
7346
7644
  }], filterEvent: [{
7347
7645
  type: Output
7348
7646
  }], focusEvent: [{
@@ -7368,20 +7666,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7368
7666
  * @input props: InputMetadata - Configuration for the select input (form control, label, options, etc.)
7369
7667
  */
7370
7668
  class SearchSelectorComponent {
7669
+ constructor() {
7670
+ this.i18n = inject(I18nService);
7671
+ }
7371
7672
  ngOnInit() {
7372
7673
  if (this.props?.withDefault || this.props?.value) {
7373
7674
  applyDefaultValueToControl(this.props);
7374
7675
  }
7375
- // Set modal header from props or use label as fallback
7376
- const headerText = this.props.modalHeader || this.props.label || 'Seleccionar opción';
7676
+ // Set modal header from props or use label as fallback, then i18n default
7677
+ const headerText = this.props.modalHeader || this.props.label || this.i18n.t('selectOption');
7377
7678
  this.customModalOptions = {
7378
7679
  header: headerText,
7379
7680
  breakpoints: [0, 0.6],
7380
7681
  initialBreakpoint: 0.6,
7381
7682
  };
7382
- // Set button texts from props or use defaults
7383
- this.cancelText = this.props.cancelText || 'Cancelar';
7384
- this.okText = this.props.okText || 'Aceptar';
7683
+ // Set button texts from props or use i18n defaults
7684
+ this.cancelText = this.props.cancelText || this.i18n.t('cancel');
7685
+ this.okText = this.props.okText || this.i18n.t('ok');
7385
7686
  }
7386
7687
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SearchSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
7387
7688
  static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: SearchSelectorComponent, isStandalone: true, selector: "val-select-input", inputs: { props: "props" }, ngImport: i0, template: `
@@ -7452,6 +7753,7 @@ class SelectSearchComponent {
7452
7753
  this.placeholder = 'Seleccione una opción';
7453
7754
  this.icon = inject(IconService);
7454
7755
  this.changeDetector = inject(ChangeDetectorRef);
7756
+ this.i18n = inject(I18nService);
7455
7757
  this.searchTerm = '';
7456
7758
  this.filteredItems = [];
7457
7759
  this.selectedItems = [];
@@ -7459,6 +7761,18 @@ class SelectSearchComponent {
7459
7761
  this.previousOptions = [];
7460
7762
  this.isProcessingChanges = false;
7461
7763
  }
7764
+ /** Get close button text */
7765
+ getCloseText() {
7766
+ return this.i18n.t('close');
7767
+ }
7768
+ /** Get no results text */
7769
+ getNoResultsText() {
7770
+ return this.i18n.t('noResults');
7771
+ }
7772
+ /** Get items selected text */
7773
+ getItemsSelectedText(count) {
7774
+ return `${count} ${this.i18n.t('itemsSelected')}`;
7775
+ }
7462
7776
  ngOnInit() {
7463
7777
  this.applyDefaultValue();
7464
7778
  this.initializeItems();
@@ -7662,7 +7976,7 @@ class SelectSearchComponent {
7662
7976
  this.displayValue = this.selectedItems[0][this.labelProperty];
7663
7977
  }
7664
7978
  else {
7665
- this.displayValue = `${this.selectedItems.length} elementos seleccionados`;
7979
+ this.displayValue = this.getItemsSelectedText(this.selectedItems.length);
7666
7980
  }
7667
7981
  }
7668
7982
  else {
@@ -7721,7 +8035,7 @@ class SelectSearchComponent {
7721
8035
  <ion-toolbar>
7722
8036
  <ion-title>{{ label }}</ion-title>
7723
8037
  <ion-buttons slot="end">
7724
- <ion-button (click)="cancelModal()">Cerrar</ion-button>
8038
+ <ion-button (click)="cancelModal()">{{ getCloseText() }}</ion-button>
7725
8039
  </ion-buttons>
7726
8040
  </ion-toolbar>
7727
8041
  <ion-toolbar>
@@ -7735,13 +8049,13 @@ class SelectSearchComponent {
7735
8049
  <ion-icon *ngIf="isItemSelected(item)" name="checkmark-outline" slot="end" color="primary"></ion-icon>
7736
8050
  </ion-item>
7737
8051
  <ion-item *ngIf="filteredItems.length === 0" lines="none">
7738
- <ion-label color="dark">No se encontraron resultados</ion-label>
8052
+ <ion-label color="dark">{{ getNoResultsText() }}</ion-label>
7739
8053
  </ion-item>
7740
8054
  </ion-list>
7741
8055
  </ion-content>
7742
8056
  </ng-template>
7743
8057
  </ion-modal>
7744
- `, isInline: true, styles: ["ion-header{padding:8px 8px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: IonicModule }, { kind: "component", type: i2.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: i2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "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", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2.IonModal, selector: "ion-modal" }, { kind: "directive", type: i2.TextValueAccessor, selector: "ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["disabled"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] }); }
8058
+ `, isInline: true, styles: ["ion-header{padding:8px 8px 0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: IonicModule }, { kind: "component", type: i2.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: i2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "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", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2.IonModal, selector: "ion-modal" }, { kind: "directive", type: i2.TextValueAccessor, selector: "ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["disabled", "placeholder", "cancelText"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] }); }
7745
8059
  }
7746
8060
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SelectSearchComponent, decorators: [{
7747
8061
  type: Component,
@@ -7768,7 +8082,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7768
8082
  <ion-toolbar>
7769
8083
  <ion-title>{{ label }}</ion-title>
7770
8084
  <ion-buttons slot="end">
7771
- <ion-button (click)="cancelModal()">Cerrar</ion-button>
8085
+ <ion-button (click)="cancelModal()">{{ getCloseText() }}</ion-button>
7772
8086
  </ion-buttons>
7773
8087
  </ion-toolbar>
7774
8088
  <ion-toolbar>
@@ -7782,7 +8096,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
7782
8096
  <ion-icon *ngIf="isItemSelected(item)" name="checkmark-outline" slot="end" color="primary"></ion-icon>
7783
8097
  </ion-item>
7784
8098
  <ion-item *ngIf="filteredItems.length === 0" lines="none">
7785
- <ion-label color="dark">No se encontraron resultados</ion-label>
8099
+ <ion-label color="dark">{{ getNoResultsText() }}</ion-label>
7786
8100
  </ion-item>
7787
8101
  </ion-list>
7788
8102
  </ion-content>
@@ -8009,6 +8323,7 @@ class CodeDisplayComponent {
8009
8323
  constructor(cdr) {
8010
8324
  this.cdr = cdr;
8011
8325
  this.toast = inject(ToastController);
8326
+ this.i18n = inject(I18nService);
8012
8327
  this.selectedTab = 0;
8013
8328
  }
8014
8329
  ngOnChanges(changes) {
@@ -8036,7 +8351,7 @@ class CodeDisplayComponent {
8036
8351
  try {
8037
8352
  const code = this.props.tabs.length > 0 ? this.props.tabs[this.selectedTab]?.code : this.props.code;
8038
8353
  await Clipboard.write({ string: code || '' });
8039
- this.presentToast('¡Copiado al portapapeles!');
8354
+ this.presentToast(this.i18n.t('copiedToClipboard'));
8040
8355
  }
8041
8356
  catch (error) {
8042
8357
  console.error('Error al copiar al portapapeles:', error);
@@ -10233,6 +10548,7 @@ class MultiSelectSearchComponent {
10233
10548
  this.placeholder = 'Seleccione opciones';
10234
10549
  this.icon = inject(IconService);
10235
10550
  this.changeDetector = inject(ChangeDetectorRef);
10551
+ this.i18n = inject(I18nService);
10236
10552
  this.searchTerm = '';
10237
10553
  this.filteredItems = [];
10238
10554
  this.selectedItems = [];
@@ -10240,6 +10556,30 @@ class MultiSelectSearchComponent {
10240
10556
  this.previousOptions = [];
10241
10557
  this.isProcessingChanges = false;
10242
10558
  }
10559
+ /** Get close button text */
10560
+ getCloseText() {
10561
+ return this.i18n.t('close');
10562
+ }
10563
+ /** Get no results text */
10564
+ getNoResultsText() {
10565
+ return this.i18n.t('noResults');
10566
+ }
10567
+ /** Get select all text */
10568
+ getSelectAllText() {
10569
+ return this.i18n.t('selectAll');
10570
+ }
10571
+ /** Get clear text */
10572
+ getClearText() {
10573
+ return this.i18n.t('clear');
10574
+ }
10575
+ /** Get apply text */
10576
+ getApplyText() {
10577
+ return this.i18n.t('apply');
10578
+ }
10579
+ /** Get items selected text */
10580
+ getItemsSelectedText(count) {
10581
+ return `${count} ${this.i18n.t('itemsSelected')}`;
10582
+ }
10243
10583
  ngOnInit() {
10244
10584
  this.applyDefaultValue();
10245
10585
  this.initializeItems();
@@ -10452,7 +10792,7 @@ class MultiSelectSearchComponent {
10452
10792
  this.displayValue = this.selectedItems[0][this.labelProperty];
10453
10793
  }
10454
10794
  else {
10455
- this.displayValue = `${this.selectedItems.length} elementos seleccionados`;
10795
+ this.displayValue = this.getItemsSelectedText(this.selectedItems.length);
10456
10796
  }
10457
10797
  }
10458
10798
  applyChanges() {
@@ -10509,7 +10849,7 @@ class MultiSelectSearchComponent {
10509
10849
  <ion-toolbar>
10510
10850
  <ion-title>{{ label }}</ion-title>
10511
10851
  <ion-buttons slot="end">
10512
- <ion-button (click)="cancelModal()">Cerrar</ion-button>
10852
+ <ion-button (click)="cancelModal()">{{ getCloseText() }}</ion-button>
10513
10853
  </ion-buttons>
10514
10854
  </ion-toolbar>
10515
10855
  <ion-toolbar>
@@ -10519,22 +10859,22 @@ class MultiSelectSearchComponent {
10519
10859
  <ion-content>
10520
10860
  <!-- Action buttons for multi-select -->
10521
10861
  <div class="actions-container" style="padding: 16px; border-bottom: 1px solid var(--ion-color-light-shade);">
10522
- <ion-button
10523
- fill="clear"
10524
- size="small"
10862
+ <ion-button
10863
+ fill="clear"
10864
+ size="small"
10525
10865
  (click)="selectAll()"
10526
10866
  [disabled]="filteredItems.length === 0"
10527
10867
  >
10528
- Seleccionar todos
10868
+ {{ getSelectAllText() }}
10529
10869
  </ion-button>
10530
- <ion-button
10531
- fill="clear"
10532
- size="small"
10533
- color="medium"
10870
+ <ion-button
10871
+ fill="clear"
10872
+ size="small"
10873
+ color="medium"
10534
10874
  (click)="clearAll()"
10535
10875
  [disabled]="selectedItems.length === 0"
10536
10876
  >
10537
- Limpiar
10877
+ {{ getClearText() }}
10538
10878
  </ion-button>
10539
10879
  </div>
10540
10880
 
@@ -10547,20 +10887,20 @@ class MultiSelectSearchComponent {
10547
10887
  <ion-label>{{ item[labelProperty] }}</ion-label>
10548
10888
  </ion-item>
10549
10889
  <ion-item *ngIf="filteredItems.length === 0" lines="none">
10550
- <ion-label color="dark">No se encontraron resultados</ion-label>
10890
+ <ion-label color="dark">{{ getNoResultsText() }}</ion-label>
10551
10891
  </ion-item>
10552
10892
  </ion-list>
10553
10893
  </ion-content>
10554
10894
  <ion-footer>
10555
10895
  <ion-toolbar>
10556
10896
  <ion-button expand="full" (click)="applyAndClose()">
10557
- Aplicar
10897
+ {{ getApplyText() }}
10558
10898
  </ion-button>
10559
10899
  </ion-toolbar>
10560
10900
  </ion-footer>
10561
10901
  </ng-template>
10562
10902
  </ion-modal>
10563
- `, isInline: true, styles: ["ion-header{padding:8px 8px 0}.actions-container{display:flex;justify-content:space-between;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: IonicModule }, { kind: "component", type: i2.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: i2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2.IonCheckbox, selector: "ion-checkbox", inputs: ["alignment", "checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "required", "value"] }, { kind: "component", type: i2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "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", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2.IonModal, selector: "ion-modal" }, { kind: "directive", type: i2.BooleanValueAccessor, selector: "ion-checkbox,ion-toggle" }, { kind: "directive", type: i2.TextValueAccessor, selector: "ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["disabled"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] }); }
10903
+ `, isInline: true, styles: ["ion-header{padding:8px 8px 0}.actions-container{display:flex;justify-content:space-between;align-items:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: IonicModule }, { kind: "component", type: i2.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: i2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2.IonCheckbox, selector: "ion-checkbox", inputs: ["alignment", "checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "required", "value"] }, { kind: "component", type: i2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2.IonInput, selector: "ion-input", inputs: ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearInputIcon", "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", "spellcheck", "step", "type", "value"] }, { kind: "component", type: i2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i2.IonModal, selector: "ion-modal" }, { kind: "directive", type: i2.BooleanValueAccessor, selector: "ion-checkbox,ion-toggle" }, { kind: "directive", type: i2.TextValueAccessor, selector: "ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$3.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: SearchbarComponent, selector: "val-searchbar", inputs: ["disabled", "placeholder", "cancelText"], outputs: ["filterEvent", "focusEvent", "blurEvent"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$3.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }] }); }
10564
10904
  }
10565
10905
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: MultiSelectSearchComponent, decorators: [{
10566
10906
  type: Component,
@@ -10587,7 +10927,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
10587
10927
  <ion-toolbar>
10588
10928
  <ion-title>{{ label }}</ion-title>
10589
10929
  <ion-buttons slot="end">
10590
- <ion-button (click)="cancelModal()">Cerrar</ion-button>
10930
+ <ion-button (click)="cancelModal()">{{ getCloseText() }}</ion-button>
10591
10931
  </ion-buttons>
10592
10932
  </ion-toolbar>
10593
10933
  <ion-toolbar>
@@ -10597,22 +10937,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
10597
10937
  <ion-content>
10598
10938
  <!-- Action buttons for multi-select -->
10599
10939
  <div class="actions-container" style="padding: 16px; border-bottom: 1px solid var(--ion-color-light-shade);">
10600
- <ion-button
10601
- fill="clear"
10602
- size="small"
10940
+ <ion-button
10941
+ fill="clear"
10942
+ size="small"
10603
10943
  (click)="selectAll()"
10604
10944
  [disabled]="filteredItems.length === 0"
10605
10945
  >
10606
- Seleccionar todos
10946
+ {{ getSelectAllText() }}
10607
10947
  </ion-button>
10608
- <ion-button
10609
- fill="clear"
10610
- size="small"
10611
- color="medium"
10948
+ <ion-button
10949
+ fill="clear"
10950
+ size="small"
10951
+ color="medium"
10612
10952
  (click)="clearAll()"
10613
10953
  [disabled]="selectedItems.length === 0"
10614
10954
  >
10615
- Limpiar
10955
+ {{ getClearText() }}
10616
10956
  </ion-button>
10617
10957
  </div>
10618
10958
 
@@ -10625,14 +10965,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
10625
10965
  <ion-label>{{ item[labelProperty] }}</ion-label>
10626
10966
  </ion-item>
10627
10967
  <ion-item *ngIf="filteredItems.length === 0" lines="none">
10628
- <ion-label color="dark">No se encontraron resultados</ion-label>
10968
+ <ion-label color="dark">{{ getNoResultsText() }}</ion-label>
10629
10969
  </ion-item>
10630
10970
  </ion-list>
10631
10971
  </ion-content>
10632
10972
  <ion-footer>
10633
10973
  <ion-toolbar>
10634
10974
  <ion-button expand="full" (click)="applyAndClose()">
10635
- Aplicar
10975
+ {{ getApplyText() }}
10636
10976
  </ion-button>
10637
10977
  </ion-toolbar>
10638
10978
  </ion-footer>
@@ -11700,8 +12040,32 @@ class DateRangeInputComponent {
11700
12040
  this.startDatetimeId = `start-datetime-${Math.random().toString(36).substr(2, 9)}`;
11701
12041
  this.endDatetimeId = `end-datetime-${Math.random().toString(36).substr(2, 9)}`;
11702
12042
  this.showDayCount = true;
12043
+ this.i18n = inject(I18nService);
11703
12044
  this.valueSubscription = null;
11704
12045
  }
12046
+ /** Get done button text from props or i18n */
12047
+ getDoneText() {
12048
+ return this.props.doneText || this.i18n.t('ok');
12049
+ }
12050
+ /** Get cancel button text from props or i18n */
12051
+ getCancelText() {
12052
+ return this.props.cancelText || this.i18n.t('cancel');
12053
+ }
12054
+ /** Get start date fallback label */
12055
+ getStartDateFallback() {
12056
+ return this.i18n.t('startDate', '_global') || 'Start date';
12057
+ }
12058
+ /** Get end date fallback label */
12059
+ getEndDateFallback() {
12060
+ return this.i18n.t('endDate', '_global') || 'End date';
12061
+ }
12062
+ /** Get day/days label based on count */
12063
+ getDayLabel() {
12064
+ if (this.dayCount === 1) {
12065
+ return this.i18n.t('day', '_global') || 'day';
12066
+ }
12067
+ return this.i18n.t('days', '_global') || 'days';
12068
+ }
11705
12069
  ngOnInit() {
11706
12070
  // Use provided controls or internal ones
11707
12071
  if (this.props.startControl) {
@@ -11874,11 +12238,11 @@ class DateRangeInputComponent {
11874
12238
  [min]="getMinDate()"
11875
12239
  [max]="getMaxStartDate()"
11876
12240
  [showDefaultButtons]="true"
11877
- [doneText]="props.doneText || 'Aceptar'"
11878
- [cancelText]="props.cancelText || 'Cancelar'"
12241
+ [doneText]="getDoneText()"
12242
+ [cancelText]="getCancelText()"
11879
12243
  (ionChange)="onStartDateChange($event)"
11880
12244
  >
11881
- <span slot="title">{{ getStartLabel() || 'Fecha inicio' }}</span>
12245
+ <span slot="title">{{ getStartLabel() || getStartDateFallback() }}</span>
11882
12246
  </ion-datetime>
11883
12247
  </ng-template>
11884
12248
  </ion-modal>
@@ -11908,11 +12272,11 @@ class DateRangeInputComponent {
11908
12272
  [min]="getMinEndDate()"
11909
12273
  [max]="getMaxDate()"
11910
12274
  [showDefaultButtons]="true"
11911
- [doneText]="props.doneText || 'Aceptar'"
11912
- [cancelText]="props.cancelText || 'Cancelar'"
12275
+ [doneText]="getDoneText()"
12276
+ [cancelText]="getCancelText()"
11913
12277
  (ionChange)="onEndDateChange($event)"
11914
12278
  >
11915
- <span slot="title">{{ getEndLabel() || 'Fecha fin' }}</span>
12279
+ <span slot="title">{{ getEndLabel() || getEndDateFallback() }}</span>
11916
12280
  </ion-datetime>
11917
12281
  </ng-template>
11918
12282
  </ion-modal>
@@ -11921,7 +12285,7 @@ class DateRangeInputComponent {
11921
12285
 
11922
12286
  @if (showDayCount && dayCount !== null) {
11923
12287
  <div class="day-count">
11924
- {{ dayCount }} {{ dayCount === 1 ? 'día' : 'días' }}
12288
+ {{ dayCount }} {{ getDayLabel() }}
11925
12289
  </div>
11926
12290
  }
11927
12291
 
@@ -11972,11 +12336,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
11972
12336
  [min]="getMinDate()"
11973
12337
  [max]="getMaxStartDate()"
11974
12338
  [showDefaultButtons]="true"
11975
- [doneText]="props.doneText || 'Aceptar'"
11976
- [cancelText]="props.cancelText || 'Cancelar'"
12339
+ [doneText]="getDoneText()"
12340
+ [cancelText]="getCancelText()"
11977
12341
  (ionChange)="onStartDateChange($event)"
11978
12342
  >
11979
- <span slot="title">{{ getStartLabel() || 'Fecha inicio' }}</span>
12343
+ <span slot="title">{{ getStartLabel() || getStartDateFallback() }}</span>
11980
12344
  </ion-datetime>
11981
12345
  </ng-template>
11982
12346
  </ion-modal>
@@ -12006,11 +12370,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
12006
12370
  [min]="getMinEndDate()"
12007
12371
  [max]="getMaxDate()"
12008
12372
  [showDefaultButtons]="true"
12009
- [doneText]="props.doneText || 'Aceptar'"
12010
- [cancelText]="props.cancelText || 'Cancelar'"
12373
+ [doneText]="getDoneText()"
12374
+ [cancelText]="getCancelText()"
12011
12375
  (ionChange)="onEndDateChange($event)"
12012
12376
  >
12013
- <span slot="title">{{ getEndLabel() || 'Fecha fin' }}</span>
12377
+ <span slot="title">{{ getEndLabel() || getEndDateFallback() }}</span>
12014
12378
  </ion-datetime>
12015
12379
  </ng-template>
12016
12380
  </ion-modal>
@@ -12019,7 +12383,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
12019
12383
 
12020
12384
  @if (showDayCount && dayCount !== null) {
12021
12385
  <div class="day-count">
12022
- {{ dayCount }} {{ dayCount === 1 ? 'día' : 'días' }}
12386
+ {{ dayCount }} {{ getDayLabel() }}
12023
12387
  </div>
12024
12388
  }
12025
12389
 
@@ -14186,8 +14550,25 @@ class ParticipantCardComponent {
14186
14550
  constructor() {
14187
14551
  this.cardClick = new EventEmitter();
14188
14552
  this.actionClick = new EventEmitter();
14553
+ this.i18n = inject(I18nService);
14189
14554
  this.showAllTickets = false;
14190
14555
  }
14556
+ /** Get winner badge text */
14557
+ getWinnerText() {
14558
+ return this.i18n.t('winner');
14559
+ }
14560
+ /** Get tickets count text */
14561
+ getTicketsCountText(count) {
14562
+ return `${count} ${count !== 1 ? this.i18n.t('tickets') : this.i18n.t('ticket')}`;
14563
+ }
14564
+ /** Get more tickets text */
14565
+ getMoreText(count) {
14566
+ return `+${count} ${this.i18n.t('more')}`;
14567
+ }
14568
+ /** Get notes label */
14569
+ getNotesLabel() {
14570
+ return this.i18n.t('notes');
14571
+ }
14191
14572
  get ticketNumbers() {
14192
14573
  return this.props.participant.tickets || [];
14193
14574
  }
@@ -14312,7 +14693,7 @@ class ParticipantCardComponent {
14312
14693
  @if (props.participant.isWinner && props.highlightWinner !== false) {
14313
14694
  <div class="winner-badge">
14314
14695
  <ion-icon name="trophy-outline"></ion-icon>
14315
- <span>Ganador</span>
14696
+ <span>{{ getWinnerText() }}</span>
14316
14697
  </div>
14317
14698
  }
14318
14699
 
@@ -14397,7 +14778,7 @@ class ParticipantCardComponent {
14397
14778
  <div class="tickets-section">
14398
14779
  <div class="tickets-header">
14399
14780
  <ion-icon name="ticket-outline"></ion-icon>
14400
- <span>{{ ticketNumbers.length }} boleto{{ ticketNumbers.length !== 1 ? 's' : '' }}</span>
14781
+ <span>{{ getTicketsCountText(ticketNumbers.length) }}</span>
14401
14782
  </div>
14402
14783
 
14403
14784
  <div class="tickets-list">
@@ -14420,7 +14801,7 @@ class ParticipantCardComponent {
14420
14801
  class="more-tickets"
14421
14802
  (click)="toggleShowAllTickets($event)"
14422
14803
  >
14423
- +{{ hiddenTicketsCount }} más
14804
+ {{ getMoreText(hiddenTicketsCount) }}
14424
14805
  </ion-chip>
14425
14806
  }
14426
14807
  </div>
@@ -14430,7 +14811,7 @@ class ParticipantCardComponent {
14430
14811
  <!-- Notes (admin variant) -->
14431
14812
  @if (props.variant === 'admin' && props.participant.notes) {
14432
14813
  <div class="notes-section">
14433
- <span class="notes-label">Notas:</span>
14814
+ <span class="notes-label">{{ getNotesLabel() }}</span>
14434
14815
  <p class="notes-text">{{ props.participant.notes }}</p>
14435
14816
  </div>
14436
14817
  }
@@ -14458,7 +14839,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
14458
14839
  @if (props.participant.isWinner && props.highlightWinner !== false) {
14459
14840
  <div class="winner-badge">
14460
14841
  <ion-icon name="trophy-outline"></ion-icon>
14461
- <span>Ganador</span>
14842
+ <span>{{ getWinnerText() }}</span>
14462
14843
  </div>
14463
14844
  }
14464
14845
 
@@ -14543,7 +14924,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
14543
14924
  <div class="tickets-section">
14544
14925
  <div class="tickets-header">
14545
14926
  <ion-icon name="ticket-outline"></ion-icon>
14546
- <span>{{ ticketNumbers.length }} boleto{{ ticketNumbers.length !== 1 ? 's' : '' }}</span>
14927
+ <span>{{ getTicketsCountText(ticketNumbers.length) }}</span>
14547
14928
  </div>
14548
14929
 
14549
14930
  <div class="tickets-list">
@@ -14566,7 +14947,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
14566
14947
  class="more-tickets"
14567
14948
  (click)="toggleShowAllTickets($event)"
14568
14949
  >
14569
- +{{ hiddenTicketsCount }} más
14950
+ {{ getMoreText(hiddenTicketsCount) }}
14570
14951
  </ion-chip>
14571
14952
  }
14572
14953
  </div>
@@ -14576,7 +14957,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
14576
14957
  <!-- Notes (admin variant) -->
14577
14958
  @if (props.variant === 'admin' && props.participant.notes) {
14578
14959
  <div class="notes-section">
14579
- <span class="notes-label">Notas:</span>
14960
+ <span class="notes-label">{{ getNotesLabel() }}</span>
14580
14961
  <p class="notes-text">{{ props.participant.notes }}</p>
14581
14962
  </div>
14582
14963
  }
@@ -18706,6 +19087,7 @@ class DataTableComponent {
18706
19087
  /** Cached visible columns for performance */
18707
19088
  this._visibleColumns = [];
18708
19089
  this.cdr = inject(ChangeDetectorRef);
19090
+ this.i18n = inject(I18nService);
18709
19091
  }
18710
19092
  ngOnInit() {
18711
19093
  this.initializeState();
@@ -18785,7 +19167,40 @@ class DataTableComponent {
18785
19167
  return count;
18786
19168
  }
18787
19169
  get emptyState() {
18788
- return this.props.emptyState || DEFAULT_EMPTY_STATE;
19170
+ const defaultEmptyState = {
19171
+ icon: 'document-outline',
19172
+ title: this.i18n.t('noData'),
19173
+ description: this.i18n.t('noRecordsFound'),
19174
+ };
19175
+ return this.props.emptyState || defaultEmptyState;
19176
+ }
19177
+ /** Get actions column label */
19178
+ getActionsLabel() {
19179
+ return this.props.actionsLabel || this.i18n.t('actions');
19180
+ }
19181
+ /** Get pagination info text */
19182
+ getPaginationInfoText() {
19183
+ return `${this.i18n.t('showing')} ${this.paginationStart}-${this.paginationEnd} ${this.i18n.t('of')} ${this.props.pagination?.total}`;
19184
+ }
19185
+ /** Get per page text */
19186
+ getPerPageText(size) {
19187
+ return `${size} ${this.i18n.t('perPage')}`;
19188
+ }
19189
+ /** Get first page aria label */
19190
+ getFirstPageLabel() {
19191
+ return this.i18n.t('firstPage');
19192
+ }
19193
+ /** Get previous page aria label */
19194
+ getPreviousPageLabel() {
19195
+ return this.i18n.t('previousPage');
19196
+ }
19197
+ /** Get next page aria label */
19198
+ getNextPageLabel() {
19199
+ return this.i18n.t('nextPage');
19200
+ }
19201
+ /** Get last page aria label */
19202
+ getLastPageLabel() {
19203
+ return this.i18n.t('lastPage');
18789
19204
  }
18790
19205
  get pageSizeOptions() {
18791
19206
  return this.props.pagination?.pageSizeOptions || DEFAULT_PAGE_SIZE_OPTIONS;
@@ -19073,7 +19488,7 @@ class DataTableComponent {
19073
19488
  <!-- Actions column -->
19074
19489
  @if (props.actionsTemplate) {
19075
19490
  <th class="actions-cell" [style.width]="props.actionsWidth || '100px'">
19076
- {{ props.actionsLabel || 'Acciones' }}
19491
+ {{ getActionsLabel() }}
19077
19492
  </th>
19078
19493
  }
19079
19494
  </tr>
@@ -19244,7 +19659,7 @@ class DataTableComponent {
19244
19659
  <div class="pagination-container">
19245
19660
  <div class="pagination-info">
19246
19661
  <span>
19247
- Mostrando {{ paginationStart }}-{{ paginationEnd }} de {{ props.pagination.total }}
19662
+ {{ getPaginationInfoText() }}
19248
19663
  </span>
19249
19664
 
19250
19665
  @if (pageSizeOptions.length > 1) {
@@ -19255,7 +19670,7 @@ class DataTableComponent {
19255
19670
  class="page-size-select"
19256
19671
  >
19257
19672
  @for (size of pageSizeOptions; track size) {
19258
- <ion-select-option [value]="size">{{ size }} por página</ion-select-option>
19673
+ <ion-select-option [value]="size">{{ getPerPageText(size) }}</ion-select-option>
19259
19674
  }
19260
19675
  </ion-select>
19261
19676
  }
@@ -19267,7 +19682,7 @@ class DataTableComponent {
19267
19682
  size="small"
19268
19683
  [disabled]="props.pagination.page === 0"
19269
19684
  (click)="goToPage(0)"
19270
- aria-label="Primera página"
19685
+ [attr.aria-label]="getFirstPageLabel()"
19271
19686
  >
19272
19687
  <ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
19273
19688
  <ion-icon slot="icon-only" name="chevron-back-outline" style="margin-left: -12px"></ion-icon>
@@ -19278,7 +19693,7 @@ class DataTableComponent {
19278
19693
  size="small"
19279
19694
  [disabled]="props.pagination.page === 0"
19280
19695
  (click)="goToPage(props.pagination.page - 1)"
19281
- aria-label="Página anterior"
19696
+ [attr.aria-label]="getPreviousPageLabel()"
19282
19697
  >
19283
19698
  <ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
19284
19699
  </ion-button>
@@ -19292,7 +19707,7 @@ class DataTableComponent {
19292
19707
  size="small"
19293
19708
  [disabled]="props.pagination.page >= totalPages - 1"
19294
19709
  (click)="goToPage(props.pagination.page + 1)"
19295
- aria-label="Página siguiente"
19710
+ [attr.aria-label]="getNextPageLabel()"
19296
19711
  >
19297
19712
  <ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
19298
19713
  </ion-button>
@@ -19302,7 +19717,7 @@ class DataTableComponent {
19302
19717
  size="small"
19303
19718
  [disabled]="props.pagination.page >= totalPages - 1"
19304
19719
  (click)="goToPage(totalPages - 1)"
19305
- aria-label="Última página"
19720
+ [attr.aria-label]="getLastPageLabel()"
19306
19721
  >
19307
19722
  <ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
19308
19723
  <ion-icon slot="icon-only" name="chevron-forward-outline" style="margin-left: -12px"></ion-icon>
@@ -19427,7 +19842,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19427
19842
  <!-- Actions column -->
19428
19843
  @if (props.actionsTemplate) {
19429
19844
  <th class="actions-cell" [style.width]="props.actionsWidth || '100px'">
19430
- {{ props.actionsLabel || 'Acciones' }}
19845
+ {{ getActionsLabel() }}
19431
19846
  </th>
19432
19847
  }
19433
19848
  </tr>
@@ -19598,7 +20013,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19598
20013
  <div class="pagination-container">
19599
20014
  <div class="pagination-info">
19600
20015
  <span>
19601
- Mostrando {{ paginationStart }}-{{ paginationEnd }} de {{ props.pagination.total }}
20016
+ {{ getPaginationInfoText() }}
19602
20017
  </span>
19603
20018
 
19604
20019
  @if (pageSizeOptions.length > 1) {
@@ -19609,7 +20024,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19609
20024
  class="page-size-select"
19610
20025
  >
19611
20026
  @for (size of pageSizeOptions; track size) {
19612
- <ion-select-option [value]="size">{{ size }} por página</ion-select-option>
20027
+ <ion-select-option [value]="size">{{ getPerPageText(size) }}</ion-select-option>
19613
20028
  }
19614
20029
  </ion-select>
19615
20030
  }
@@ -19621,7 +20036,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19621
20036
  size="small"
19622
20037
  [disabled]="props.pagination.page === 0"
19623
20038
  (click)="goToPage(0)"
19624
- aria-label="Primera página"
20039
+ [attr.aria-label]="getFirstPageLabel()"
19625
20040
  >
19626
20041
  <ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
19627
20042
  <ion-icon slot="icon-only" name="chevron-back-outline" style="margin-left: -12px"></ion-icon>
@@ -19632,7 +20047,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19632
20047
  size="small"
19633
20048
  [disabled]="props.pagination.page === 0"
19634
20049
  (click)="goToPage(props.pagination.page - 1)"
19635
- aria-label="Página anterior"
20050
+ [attr.aria-label]="getPreviousPageLabel()"
19636
20051
  >
19637
20052
  <ion-icon slot="icon-only" name="chevron-back-outline"></ion-icon>
19638
20053
  </ion-button>
@@ -19646,7 +20061,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19646
20061
  size="small"
19647
20062
  [disabled]="props.pagination.page >= totalPages - 1"
19648
20063
  (click)="goToPage(props.pagination.page + 1)"
19649
- aria-label="Página siguiente"
20064
+ [attr.aria-label]="getNextPageLabel()"
19650
20065
  >
19651
20066
  <ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
19652
20067
  </ion-button>
@@ -19656,7 +20071,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
19656
20071
  size="small"
19657
20072
  [disabled]="props.pagination.page >= totalPages - 1"
19658
20073
  (click)="goToPage(totalPages - 1)"
19659
- aria-label="Última página"
20074
+ [attr.aria-label]="getLastPageLabel()"
19660
20075
  >
19661
20076
  <ion-icon slot="icon-only" name="chevron-forward-outline"></ion-icon>
19662
20077
  <ion-icon slot="icon-only" name="chevron-forward-outline" style="margin-left: -12px"></ion-icon>
@@ -28958,6 +29373,22 @@ class AdSlotComponent {
28958
29373
  }
28959
29374
  return true;
28960
29375
  });
29376
+ /**
29377
+ * Indica si debe mostrar placeholder en vez de ad real.
29378
+ * Activo en localhost o con Publisher ID placeholder.
29379
+ */
29380
+ this.isPlaceholderMode = computed(() => {
29381
+ if (!isPlatformBrowser(this.platformId)) {
29382
+ return false;
29383
+ }
29384
+ const adClient = this.adsService.adClient();
29385
+ const isLocalhost = window.location.hostname === 'localhost' ||
29386
+ window.location.hostname === '127.0.0.1';
29387
+ const isPlaceholderClient = !adClient ||
29388
+ adClient === 'ca-pub-0000000000000000' ||
29389
+ adClient.includes('0000000000');
29390
+ return isLocalhost || isPlaceholderClient;
29391
+ });
28961
29392
  }
28962
29393
  // ===========================================================================
28963
29394
  // LIFECYCLE
@@ -28972,6 +29403,15 @@ class AdSlotComponent {
28972
29403
  if (!this.shouldRender() || this.adInitialized) {
28973
29404
  return;
28974
29405
  }
29406
+ // En modo placeholder, solo marcar como rendered
29407
+ if (this.isPlaceholderMode()) {
29408
+ this._state.set('rendered');
29409
+ this.adInitialized = true;
29410
+ if (this.adsService.isDebugMode()) {
29411
+ console.log(`[ValtechAds] Ad slot '${this.slotId}' in PLACEHOLDER mode (localhost or invalid adClient)`);
29412
+ }
29413
+ return;
29414
+ }
28975
29415
  await this.initializeAd();
28976
29416
  }
28977
29417
  ngOnDestroy() {
@@ -29014,37 +29454,53 @@ class AdSlotComponent {
29014
29454
  [class.val-ad-slot--rendered]="state() === 'rendered'"
29015
29455
  [class.val-ad-slot--empty]="state() === 'empty'"
29016
29456
  [class.val-ad-slot--hidden]="state() === 'hidden'"
29457
+ [class.val-ad-slot--placeholder]="isPlaceholderMode()"
29017
29458
  [class]="cssClass"
29018
29459
  [style.min-height]="minHeight"
29019
29460
  >
29020
- <!-- Skeleton mientras carga -->
29021
- @if (showSkeleton && state() === 'loading') {
29022
- <div
29023
- class="val-ad-slot__skeleton"
29024
- [style.height]="minHeight"
29025
- ></div>
29026
- }
29027
-
29028
- <!-- AdSense ins element -->
29029
- <ins
29030
- class="adsbygoogle val-ad-slot__container"
29031
- [class.val-ad-slot__container--hidden]="state() === 'loading' && showSkeleton"
29032
- [style.display]="'block'"
29033
- [attr.data-ad-client]="adsService.adClient()"
29034
- [attr.data-ad-slot]="adSlot || null"
29035
- [attr.data-ad-format]="format"
29036
- [attr.data-full-width-responsive]="fullWidth ? 'true' : null"
29037
- ></ins>
29461
+ <!-- Placeholder mode (desarrollo local) -->
29462
+ @if (isPlaceholderMode()) {
29463
+ <div class="val-ad-slot__placeholder" [style.height]="minHeight">
29464
+ <div class="val-ad-slot__placeholder-content">
29465
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
29466
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
29467
+ <line x1="3" y1="9" x2="21" y2="9"/>
29468
+ <line x1="9" y1="21" x2="9" y2="9"/>
29469
+ </svg>
29470
+ <span class="val-ad-slot__placeholder-label">Ad Placeholder</span>
29471
+ <span class="val-ad-slot__placeholder-info">{{ slotId }} | {{ format }}</span>
29472
+ </div>
29473
+ </div>
29474
+ } @else {
29475
+ <!-- Skeleton mientras carga -->
29476
+ @if (showSkeleton && state() === 'loading') {
29477
+ <div
29478
+ class="val-ad-slot__skeleton"
29479
+ [style.height]="minHeight"
29480
+ ></div>
29481
+ }
29482
+
29483
+ <!-- AdSense ins element -->
29484
+ <ins
29485
+ class="adsbygoogle val-ad-slot__container"
29486
+ [class.val-ad-slot__container--hidden]="state() === 'loading' && showSkeleton"
29487
+ [style.display]="'block'"
29488
+ [attr.data-ad-client]="adsService.adClient()"
29489
+ [attr.data-ad-slot]="adSlot || null"
29490
+ [attr.data-ad-format]="format"
29491
+ [attr.data-full-width-responsive]="fullWidth ? 'true' : null"
29492
+ ></ins>
29493
+ }
29038
29494
 
29039
29495
  <!-- Debug info -->
29040
29496
  @if (adsService.isDebugMode()) {
29041
29497
  <div class="val-ad-slot__debug">
29042
- <small>{{ slotId }} | {{ format }} | {{ state() }}</small>
29498
+ <small>{{ slotId }} | {{ format }} | {{ state() }}{{ isPlaceholderMode() ? ' | PLACEHOLDER' : '' }}</small>
29043
29499
  </div>
29044
29500
  }
29045
29501
  </div>
29046
29502
  }
29047
- `, isInline: true, styles: [".val-ad-slot{position:relative;display:flex;justify-content:center;align-items:center;overflow:hidden;width:100%}.val-ad-slot--loading{background-color:var(--ion-color-light, #f4f4f4)}.val-ad-slot--empty,.val-ad-slot--hidden{display:none!important}.val-ad-slot__skeleton{width:100%;background:linear-gradient(90deg,var(--ion-color-light, #f4f4f4) 25%,var(--ion-color-light-shade, #e0e0e0) 50%,var(--ion-color-light, #f4f4f4) 75%);background-size:200% 100%;animation:skeleton-loading 1.5s infinite;border-radius:4px}.val-ad-slot__container{width:100%}.val-ad-slot__container--hidden{visibility:hidden;position:absolute}.val-ad-slot__debug{position:absolute;bottom:0;left:0;background:#000000b3;color:#fff;padding:2px 6px;font-size:10px;z-index:1000;font-family:monospace}@keyframes skeleton-loading{0%{background-position:200% 0}to{background-position:-200% 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
29503
+ `, isInline: true, styles: [".val-ad-slot{position:relative;display:flex;justify-content:center;align-items:center;overflow:hidden;width:100%}.val-ad-slot--loading{background-color:var(--ion-color-light, #f4f4f4)}.val-ad-slot--empty,.val-ad-slot--hidden{display:none!important}.val-ad-slot__skeleton{width:100%;background:linear-gradient(90deg,var(--ion-color-light, #f4f4f4) 25%,var(--ion-color-light-shade, #e0e0e0) 50%,var(--ion-color-light, #f4f4f4) 75%);background-size:200% 100%;animation:skeleton-loading 1.5s infinite;border-radius:4px}.val-ad-slot__container{width:100%}.val-ad-slot__container--hidden{visibility:hidden;position:absolute}.val-ad-slot__placeholder{width:100%;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;border:2px dashed rgba(255,255,255,.4)}.val-ad-slot__placeholder-content{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px;text-align:center}.val-ad-slot__placeholder-content svg{opacity:.8}.val-ad-slot__placeholder-label{font-weight:600;font-size:14px;letter-spacing:.5px}.val-ad-slot__placeholder-info{font-size:11px;opacity:.7;font-family:monospace}.val-ad-slot__debug{position:absolute;bottom:0;left:0;background:#000000b3;color:#fff;padding:2px 6px;font-size:10px;z-index:1000;font-family:monospace}@keyframes skeleton-loading{0%{background-position:200% 0}to{background-position:-200% 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
29048
29504
  }
29049
29505
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AdSlotComponent, decorators: [{
29050
29506
  type: Component,
@@ -29056,37 +29512,53 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
29056
29512
  [class.val-ad-slot--rendered]="state() === 'rendered'"
29057
29513
  [class.val-ad-slot--empty]="state() === 'empty'"
29058
29514
  [class.val-ad-slot--hidden]="state() === 'hidden'"
29515
+ [class.val-ad-slot--placeholder]="isPlaceholderMode()"
29059
29516
  [class]="cssClass"
29060
29517
  [style.min-height]="minHeight"
29061
29518
  >
29062
- <!-- Skeleton mientras carga -->
29063
- @if (showSkeleton && state() === 'loading') {
29064
- <div
29065
- class="val-ad-slot__skeleton"
29066
- [style.height]="minHeight"
29067
- ></div>
29068
- }
29069
-
29070
- <!-- AdSense ins element -->
29071
- <ins
29072
- class="adsbygoogle val-ad-slot__container"
29073
- [class.val-ad-slot__container--hidden]="state() === 'loading' && showSkeleton"
29074
- [style.display]="'block'"
29075
- [attr.data-ad-client]="adsService.adClient()"
29076
- [attr.data-ad-slot]="adSlot || null"
29077
- [attr.data-ad-format]="format"
29078
- [attr.data-full-width-responsive]="fullWidth ? 'true' : null"
29079
- ></ins>
29519
+ <!-- Placeholder mode (desarrollo local) -->
29520
+ @if (isPlaceholderMode()) {
29521
+ <div class="val-ad-slot__placeholder" [style.height]="minHeight">
29522
+ <div class="val-ad-slot__placeholder-content">
29523
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
29524
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
29525
+ <line x1="3" y1="9" x2="21" y2="9"/>
29526
+ <line x1="9" y1="21" x2="9" y2="9"/>
29527
+ </svg>
29528
+ <span class="val-ad-slot__placeholder-label">Ad Placeholder</span>
29529
+ <span class="val-ad-slot__placeholder-info">{{ slotId }} | {{ format }}</span>
29530
+ </div>
29531
+ </div>
29532
+ } @else {
29533
+ <!-- Skeleton mientras carga -->
29534
+ @if (showSkeleton && state() === 'loading') {
29535
+ <div
29536
+ class="val-ad-slot__skeleton"
29537
+ [style.height]="minHeight"
29538
+ ></div>
29539
+ }
29540
+
29541
+ <!-- AdSense ins element -->
29542
+ <ins
29543
+ class="adsbygoogle val-ad-slot__container"
29544
+ [class.val-ad-slot__container--hidden]="state() === 'loading' && showSkeleton"
29545
+ [style.display]="'block'"
29546
+ [attr.data-ad-client]="adsService.adClient()"
29547
+ [attr.data-ad-slot]="adSlot || null"
29548
+ [attr.data-ad-format]="format"
29549
+ [attr.data-full-width-responsive]="fullWidth ? 'true' : null"
29550
+ ></ins>
29551
+ }
29080
29552
 
29081
29553
  <!-- Debug info -->
29082
29554
  @if (adsService.isDebugMode()) {
29083
29555
  <div class="val-ad-slot__debug">
29084
- <small>{{ slotId }} | {{ format }} | {{ state() }}</small>
29556
+ <small>{{ slotId }} | {{ format }} | {{ state() }}{{ isPlaceholderMode() ? ' | PLACEHOLDER' : '' }}</small>
29085
29557
  </div>
29086
29558
  }
29087
29559
  </div>
29088
29560
  }
29089
- `, styles: [".val-ad-slot{position:relative;display:flex;justify-content:center;align-items:center;overflow:hidden;width:100%}.val-ad-slot--loading{background-color:var(--ion-color-light, #f4f4f4)}.val-ad-slot--empty,.val-ad-slot--hidden{display:none!important}.val-ad-slot__skeleton{width:100%;background:linear-gradient(90deg,var(--ion-color-light, #f4f4f4) 25%,var(--ion-color-light-shade, #e0e0e0) 50%,var(--ion-color-light, #f4f4f4) 75%);background-size:200% 100%;animation:skeleton-loading 1.5s infinite;border-radius:4px}.val-ad-slot__container{width:100%}.val-ad-slot__container--hidden{visibility:hidden;position:absolute}.val-ad-slot__debug{position:absolute;bottom:0;left:0;background:#000000b3;color:#fff;padding:2px 6px;font-size:10px;z-index:1000;font-family:monospace}@keyframes skeleton-loading{0%{background-position:200% 0}to{background-position:-200% 0}}\n"] }]
29561
+ `, styles: [".val-ad-slot{position:relative;display:flex;justify-content:center;align-items:center;overflow:hidden;width:100%}.val-ad-slot--loading{background-color:var(--ion-color-light, #f4f4f4)}.val-ad-slot--empty,.val-ad-slot--hidden{display:none!important}.val-ad-slot__skeleton{width:100%;background:linear-gradient(90deg,var(--ion-color-light, #f4f4f4) 25%,var(--ion-color-light-shade, #e0e0e0) 50%,var(--ion-color-light, #f4f4f4) 75%);background-size:200% 100%;animation:skeleton-loading 1.5s infinite;border-radius:4px}.val-ad-slot__container{width:100%}.val-ad-slot__container--hidden{visibility:hidden;position:absolute}.val-ad-slot__placeholder{width:100%;background:linear-gradient(135deg,#667eea,#764ba2);border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff;border:2px dashed rgba(255,255,255,.4)}.val-ad-slot__placeholder-content{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px;text-align:center}.val-ad-slot__placeholder-content svg{opacity:.8}.val-ad-slot__placeholder-label{font-weight:600;font-size:14px;letter-spacing:.5px}.val-ad-slot__placeholder-info{font-size:11px;opacity:.7;font-family:monospace}.val-ad-slot__debug{position:absolute;bottom:0;left:0;background:#000000b3;color:#fff;padding:2px 6px;font-size:10px;z-index:1000;font-family:monospace}@keyframes skeleton-loading{0%{background-position:200% 0}to{background-position:-200% 0}}\n"] }]
29090
29562
  }], propDecorators: { slotId: [{
29091
29563
  type: Input,
29092
29564
  args: [{ required: true }]
@@ -29112,5 +29584,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
29112
29584
  * Generated bundle index. Do not edit.
29113
29585
  */
29114
29586
 
29115
- export { AD_SIZE_MAP, 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_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_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DeviceService, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FunHeaderComponent, GlowCardComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfoComponent, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, LocalStorageService, LocaleService, 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, 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, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RightsFooterComponent, 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, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TabsComponent, 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_FIREBASE_CONFIG, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildPath, collections, createFirebaseConfig, createGlowCardProps, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFirebase, provideValtechI18n, provideValtechPresets, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
29587
+ export { AD_SIZE_MAP, 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_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_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DeviceService, DisplayComponent, DividerComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FabComponent, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FunHeaderComponent, GlowCardComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfoComponent, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinksAccordionComponent, LinksCakeComponent, LocalStorageService, LocaleService, 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, 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, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RightsFooterComponent, 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, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TabsComponent, 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, createNumberFromToField, createTitleProps, extractPathParams, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFirebase, provideValtechI18n, provideValtechPresets, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
29116
29588
  //# sourceMappingURL=valtech-components.mjs.map