sapenlinea-components 0.9.76 → 0.9.77

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.
@@ -8,7 +8,7 @@ import PubSub from 'pubsub-js';
8
8
  import { BarChart as BarChart$1, LineChart as LineChart$1, PieChart, HeatmapChart } from 'echarts/charts';
9
9
  import * as echarts from 'echarts/core';
10
10
  import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts';
11
- import { GridComponent, TooltipComponent, GraphicComponent, DataZoomComponent, VisualMapComponent, LegendComponent } from 'echarts/components';
11
+ import { GridComponent, TooltipComponent, GraphicComponent, DataZoomComponent, VisualMapComponent, LegendComponent, TitleComponent } from 'echarts/components';
12
12
  import { CanvasRenderer } from 'echarts/renderers';
13
13
  import { DomSanitizer } from '@angular/platform-browser';
14
14
 
@@ -584,9 +584,10 @@ class DateTimePicker {
584
584
  // Estado AM/PM
585
585
  selectedAmPm = signal('AM', ...(ngDevMode ? [{ debugName: "selectedAmPm" }] : []));
586
586
  documentClickListener;
587
+ resizeListener;
587
588
  /**
588
589
  * Valor que se muestra en el campo de texto.
589
- * Siempre muestra la fecha en formato DD/MM/YYYY.
590
+ * Modo 'date': DD/MM/YYYY. Modo 'datetime': DD/MM/YYYY HH:MM (24h).
590
591
  */
591
592
  displayValue = computed(() => {
592
593
  const text = this.inputTextValue().trim();
@@ -596,10 +597,7 @@ class DateTimePicker {
596
597
  const date = this.selectedDate();
597
598
  if (!date)
598
599
  return '';
599
- const day = date.getDate().toString().padStart(2, '0');
600
- const month = (date.getMonth() + 1).toString().padStart(2, '0');
601
- const year = date.getFullYear();
602
- return `${day}/${month}/${year}`;
600
+ return this.formatDateForInput(date);
603
601
  }, ...(ngDevMode ? [{ debugName: "displayValue" }] : []));
604
602
  monthName = computed(() => {
605
603
  const months = [
@@ -654,6 +652,7 @@ class DateTimePicker {
654
652
  }
655
653
  ngOnDestroy() {
656
654
  this.removeDocumentListener();
655
+ this.removePositioningListeners();
657
656
  }
658
657
  writeValue(value) {
659
658
  let date = null;
@@ -698,11 +697,10 @@ class DateTimePicker {
698
697
  if (this.isBlocked())
699
698
  return;
700
699
  this.markAsTouched();
701
- const rect = this.elementRef.nativeElement.getBoundingClientRect();
702
- const spaceBelow = window.innerHeight - rect.bottom;
703
- this.openUpward.set(spaceBelow < 420);
700
+ this.updatePosition();
704
701
  this.isOpen.set(true);
705
702
  this.addDocumentListener();
703
+ this.addPositioningListeners();
706
704
  // Si hay fecha seleccionada, navegar a ese mes/año
707
705
  const selected = this.selectedDate();
708
706
  if (selected) {
@@ -712,9 +710,34 @@ class DateTimePicker {
712
710
  }
713
711
  close() {
714
712
  this.isOpen.set(false);
713
+ this.openUpward.set(false);
715
714
  this.removeDocumentListener();
715
+ this.removePositioningListeners();
716
716
  this.markAsTouched();
717
717
  }
718
+ updatePosition() {
719
+ const scrollY = window.scrollY;
720
+ const maxScrollY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
721
+ const canScrollDown = scrollY < maxScrollY - 1;
722
+ this.openUpward.set(!canScrollDown);
723
+ }
724
+ addPositioningListeners() {
725
+ this.removePositioningListeners();
726
+ this.ngZone.runOutsideAngular(() => {
727
+ this.resizeListener = () => {
728
+ if (this.isOpen()) {
729
+ this.ngZone.run(() => this.updatePosition());
730
+ }
731
+ };
732
+ window.addEventListener('resize', this.resizeListener);
733
+ });
734
+ }
735
+ removePositioningListeners() {
736
+ if (this.resizeListener) {
737
+ window.removeEventListener('resize', this.resizeListener);
738
+ this.resizeListener = undefined;
739
+ }
740
+ }
718
741
  selectDay(day) {
719
742
  if (!day || this.isBlocked())
720
743
  return;
@@ -868,17 +891,19 @@ class DateTimePicker {
868
891
  this.markAsTouched();
869
892
  this.dateChange.emit(newDate);
870
893
  }
871
- // Manejo de input de texto (DD/MM/YYYY)
894
+ // Manejo de input de texto (DD/MM/YYYY o DD/MM/YYYY HH:MM)
872
895
  onInputChange(event) {
873
896
  if (this.isBlocked())
874
897
  return;
875
898
  const input = event.target;
876
- let value = input.value;
899
+ const value = input.value;
900
+ const isDateTime = this.mode() === 'datetime';
877
901
  // Eliminar todo lo que no sea número
878
902
  const numbersOnly = value.replace(/\D/g, '');
879
- // Limitar a 8 dígitos (DDMMYYYY)
880
- const limitedNumbers = numbersOnly.slice(0, 8);
881
- // Formatear con barras: DD/MM/YYYY
903
+ // Limitar a 8 dígitos para fecha o 12 para datetime
904
+ const maxDigits = isDateTime ? 12 : 8;
905
+ const limitedNumbers = numbersOnly.slice(0, maxDigits);
906
+ // Formatear con separadores: DD/MM/YYYY [ HH:MM]
882
907
  let formatted = '';
883
908
  if (limitedNumbers.length > 0) {
884
909
  formatted = limitedNumbers.slice(0, 2);
@@ -888,12 +913,18 @@ class DateTimePicker {
888
913
  if (limitedNumbers.length > 4) {
889
914
  formatted += '/' + limitedNumbers.slice(4, 8);
890
915
  }
916
+ if (isDateTime && limitedNumbers.length > 8) {
917
+ formatted += ' ' + limitedNumbers.slice(8, 10);
918
+ }
919
+ if (isDateTime && limitedNumbers.length > 10) {
920
+ formatted += ':' + limitedNumbers.slice(10, 12);
921
+ }
891
922
  }
892
923
  this.inputTextValue.set(formatted);
893
924
  input.value = formatted;
894
925
  this.markAsTouched();
895
- // Solo intentamos parsear cuando tenemos DD/MM/YYYY completo (10 caracteres)
896
- if (formatted.length === 10) {
926
+ const completeLength = isDateTime ? 16 : 10;
927
+ if (formatted.length === completeLength) {
897
928
  const parsedDate = this.parseDateInput(formatted);
898
929
  if (parsedDate) {
899
930
  if (this.isDateDisabled(parsedDate)) {
@@ -913,9 +944,8 @@ class DateTimePicker {
913
944
  }
914
945
  }
915
946
  /**
916
- * Selects the date segment (DD, MM, or YYYY) under the cursor on click,
917
- * mimicking native date-input block-editing behavior.
918
- * Format: DD/MM/YYYY → segments at positions 0-1, 3-4, 6-9
947
+ * Selects the date/time segment under the cursor on click.
948
+ * Format: DD/MM/YYYY [HH:MM] → DD=0-1, MM=3-4, YYYY=6-9, HH=11-12, MM=14-15
919
949
  */
920
950
  onInputClick(event) {
921
951
  event.stopPropagation();
@@ -924,29 +954,31 @@ class DateTimePicker {
924
954
  const input = event.target;
925
955
  const pos = input.selectionStart ?? 0;
926
956
  const value = input.value;
927
- // Only do segment selection when the value looks like a date (has slashes)
928
957
  if (!value || !value.includes('/'))
929
958
  return;
930
- // Determine which segment the cursor is in
931
- // DD = 0..1, slash at 2, MM = 3..4, slash at 5, YYYY = 6..9
959
+ const hasTime = this.mode() === 'datetime' && value.includes(':');
932
960
  let start;
933
961
  let end;
934
962
  if (pos <= 2) {
935
- // Day segment
936
963
  start = 0;
937
964
  end = 2;
938
965
  }
939
966
  else if (pos <= 5) {
940
- // Month segment
941
967
  start = 3;
942
968
  end = 5;
943
969
  }
944
- else {
945
- // Year segment
970
+ else if (pos <= 10 || !hasTime) {
946
971
  start = 6;
947
972
  end = 10;
948
973
  }
949
- // Use setTimeout to override the browser's default selection behavior
974
+ else if (pos <= 13) {
975
+ start = 11;
976
+ end = 13;
977
+ }
978
+ else {
979
+ start = 14;
980
+ end = 16;
981
+ }
950
982
  setTimeout(() => {
951
983
  input.setSelectionRange(start, end);
952
984
  }, 0);
@@ -959,16 +991,27 @@ class DateTimePicker {
959
991
  parseDateInput(input) {
960
992
  if (!input || input.trim() === '')
961
993
  return null;
962
- const match = input.trim().match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
963
- if (match) {
964
- const day = parseInt(match[1], 10);
965
- const month = parseInt(match[2], 10) - 1;
966
- const year = parseInt(match[3], 10);
967
- if (day >= 1 && day <= 31 && month >= 0 && month <= 11 && year >= 1900 && year <= 2100) {
968
- const date = new Date(year, month, day);
969
- if (date.getDate() === day && date.getMonth() === month && date.getFullYear() === year) {
970
- return date;
971
- }
994
+ const trimmed = input.trim();
995
+ const dateTimeMatch = trimmed.match(/^(\d{2})\/(\d{2})\/(\d{4})\s(\d{2}):(\d{2})$/);
996
+ const dateMatch = trimmed.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
997
+ const match = dateTimeMatch || dateMatch;
998
+ if (!match)
999
+ return null;
1000
+ const day = parseInt(match[1], 10);
1001
+ const month = parseInt(match[2], 10) - 1;
1002
+ const year = parseInt(match[3], 10);
1003
+ const hour = match[4] ? parseInt(match[4], 10) : 0;
1004
+ const minute = match[5] ? parseInt(match[5], 10) : 0;
1005
+ if (day >= 1 && day <= 31 &&
1006
+ month >= 0 && month <= 11 &&
1007
+ year >= 1900 && year <= 2100 &&
1008
+ hour >= 0 && hour <= 23 &&
1009
+ minute >= 0 && minute <= 59) {
1010
+ const date = new Date(year, month, day, hour, minute);
1011
+ if (date.getDate() === day &&
1012
+ date.getMonth() === month &&
1013
+ date.getFullYear() === year) {
1014
+ return date;
972
1015
  }
973
1016
  }
974
1017
  return null;
@@ -977,7 +1020,13 @@ class DateTimePicker {
977
1020
  const day = date.getDate().toString().padStart(2, '0');
978
1021
  const month = (date.getMonth() + 1).toString().padStart(2, '0');
979
1022
  const year = date.getFullYear();
980
- return `${day}/${month}/${year}`;
1023
+ const dateStr = `${day}/${month}/${year}`;
1024
+ if (this.mode() === 'datetime') {
1025
+ const hour = date.getHours().toString().padStart(2, '0');
1026
+ const minute = date.getMinutes().toString().padStart(2, '0');
1027
+ return `${dateStr} ${hour}:${minute}`;
1028
+ }
1029
+ return dateStr;
981
1030
  }
982
1031
  updateInternalState(date) {
983
1032
  this.selectedDate.set(date);
@@ -2360,9 +2409,13 @@ class SelectCustomSearch {
2360
2409
  }, ...(ngDevMode ? [{ debugName: "filteredOptions" }] : []));
2361
2410
  onChange = (value) => { };
2362
2411
  onTouched = () => { };
2412
+ resizeListener;
2363
2413
  constructor(elementRef) {
2364
2414
  this.elementRef = elementRef;
2365
2415
  }
2416
+ ngOnDestroy() {
2417
+ this.removePositioningListeners();
2418
+ }
2366
2419
  writeValue(value) {
2367
2420
  this.selectedValue.set(value);
2368
2421
  }
@@ -2388,20 +2441,41 @@ class SelectCustomSearch {
2388
2441
  open() {
2389
2442
  if (this.isBlocked())
2390
2443
  return;
2391
- const rect = this.elementRef.nativeElement.getBoundingClientRect();
2392
- const spaceBelow = window.innerHeight - rect.bottom;
2393
- this.openUpward.set(spaceBelow < 260);
2444
+ this.updatePosition();
2394
2445
  this.isOpen.set(true);
2395
2446
  this.searchTerm.set('');
2396
- setTimeout(() => {
2447
+ requestAnimationFrame(() => {
2397
2448
  const input = this.elementRef.nativeElement.querySelector('.search-input');
2398
2449
  if (input)
2399
- input.focus();
2450
+ input.focus({ preventScroll: true });
2400
2451
  });
2452
+ this.addPositioningListeners();
2401
2453
  }
2402
2454
  close() {
2403
2455
  this.isOpen.set(false);
2404
2456
  this.searchTerm.set('');
2457
+ this.openUpward.set(false);
2458
+ this.removePositioningListeners();
2459
+ }
2460
+ updatePosition() {
2461
+ const scrollY = window.scrollY;
2462
+ const maxScrollY = Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
2463
+ const canScrollDown = scrollY < maxScrollY - 1;
2464
+ this.openUpward.set(!canScrollDown);
2465
+ }
2466
+ addPositioningListeners() {
2467
+ this.removePositioningListeners();
2468
+ this.resizeListener = () => {
2469
+ if (this.isOpen())
2470
+ this.updatePosition();
2471
+ };
2472
+ window.addEventListener('resize', this.resizeListener);
2473
+ }
2474
+ removePositioningListeners() {
2475
+ if (this.resizeListener) {
2476
+ window.removeEventListener('resize', this.resizeListener);
2477
+ this.resizeListener = undefined;
2478
+ }
2405
2479
  }
2406
2480
  selectOption(option) {
2407
2481
  if (this.isBlocked())
@@ -2717,11 +2791,11 @@ class DynamicFormFields {
2717
2791
  submitBtn?.click();
2718
2792
  }
2719
2793
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DynamicFormFields, deps: [], target: i0.ɵɵFactoryTarget.Component });
2720
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: DynamicFormFields, isStandalone: true, selector: "lib-dynamic-form-fields", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (form()) {\n<form class=\"form\" [formGroup]=\"form()\" [class.form--compact]=\"compact()\" (keydown)=\"onEnter($event)\">\n @for (sec of sections(); track $index) {\n <section class=\"section\">\n @if (sec.title) {\n <h3 class=\"section-title\">{{ sec.title }}</h3>\n } @if (sec.description) {\n <p class=\"section-desc\">{{ sec.description }}</p>\n }\n\n <div class=\"grid\">\n @for (run of getFieldRuns(sec.fields); track $index) {\n\n @if (run.isGroup) {\n <!-- toggle: wrapper que siempre ocupa la fila completa -->\n <div class=\"col col--group-wrapper\">\n @for (f of run.fields; track f.key) {\n <div class=\"group-item\">\n <lib-toggle-custom [key]=\"f.key\" [label]=\"f.label ?? ''\" [formControlName]=\"f.key\" />\n </div>\n }\n </div>\n\n } @else {\n <!-- Campos normales (incluye checkbox y radio con su col configurable) -->\n @for (f of run.fields; track f.key) {\n <div class=\"col\" [style.--col-span]=\"f.col || 6\">\n @if (['text','number','email', 'password', 'time'].includes(f.type))\n {\n <input\n class=\"input\"\n [type]=\"f.type\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n (input)=\"onUppercaseInput($event, f.type, f.key)\"\n />\n } @if (['date', 'datetime-local'].includes(f.type)) {\n <lib-date-time-picker\n [mode]=\"getDatePickerMode(f.type)\"\n [placeholder]=\"\n f.placeholder ||\n (f.type === 'date'\n ? 'Seleccionar fecha'\n : 'Seleccionar fecha y hora')\n \"\n [formControlName]=\"f.key\"\n [minDate]=\"f.minDate || null\"\n [maxDate]=\"f.maxDate || null\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'textarea') {\n <textarea\n class=\"input textarea\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n rows=\"6\"\n [readonly]=\"f.readonly || false\"\n ></textarea>\n } @if (f.type === 'select') {\n <lib-select-custom-search\n [options]=\"f.options ?? []\"\n [placeholder]=\"f.placeholder || 'Seleccionar...'\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'radio') {\n <div class=\"radio-group\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"radio\">\n <input\n type=\"radio\"\n [value]=\"o.value\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n <span class=\"radio-mark\"></span>\n <span class=\"radio-label\">{{ o.label }}</span>\n </label>\n }\n </div>\n } @if (f.type === 'checkbox') {\n <div class=\"checkbox-group\"\n [class.checkbox-group--cards]=\"f.variant === 'cards'\"\n [class.checkbox-group--lowercase]=\"f.uppercase === false\"\n [class.checkbox-group--readonly]=\"f.readonly && !f.disabled\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"getCheckboxArray(f.key)?.at($index)?.value\"\n (change)=\"onCheckboxInteract($event, f, $index)\"\n [disabled]=\"f.disabled || f.readonly || false\"\n />\n <span class=\"checkbox-mark\"></span>\n <span class=\"checkbox-text\">\n <span class=\"checkbox-label\">{{ o.label }}</span>\n @if (o.subtitle) {\n <span class=\"checkbox-subtitle\">{{ o.subtitle }}</span>\n }\n </span>\n </label>\n }\n </div>\n } @if (f.type === 'disabled') {\n <input\n class=\"input input--disabled\"\n [placeholder]=\"f.placeholder || 'Autom\u00E1tico'\"\n disabled\n />\n } @if (ctrl(f.key)?.touched && ctrl(f.key)?.invalid) {\n <div class=\"error\">\n @if (ctrl(f.key)?.errors?.['required']) {\n <span>Campo requerido</span>\n } @if (ctrl(f.key)?.errors?.['email']) {\n <span>Correo inv\u00E1lido</span>\n } @if (ctrl(f.key)?.errors?.['pattern']) {\n <span>\n @switch (f.patternType) { @case ('numbers') { Solo se permiten\n n\u00FAmeros } @case ('phone') { Formato de tel\u00E9fono inv\u00E1lido } @case\n ('text') { Solo se permiten letras y espacios } @case ('username') {\n Solo se permiten letras, n\u00FAmeros, puntos y guiones bajos (no al\n inicio ni al final) } @case ('alphanumeric') { Solo se permiten\n letras y n\u00FAmeros } @default { Formato inv\u00E1lido } }\n </span>\n } @if (ctrl(f.key)?.errors?.['notMatching']) {\n <span>Las contrase\u00F1as no coinciden</span>\n }\n </div>\n }\n\n @if(f.label && f.type !== 'toggle') {\n <label\n [class.label-radio]=\"f.type === 'radio'\"\n [class.label-checkbox]=\"f.type === 'checkbox'\"\n [class.label-disabled]=\"f.readonly || f.disabled\"\n class=\"label\"\n >\n {{ f.label }} </label>\n }\n </div>\n }\n }\n\n }\n </div>\n </section>\n }\n</form>\n}\n", styles: [".form{width:100%}.form{display:grid}.section{padding:20px 0 0}.section-title{font-size:1.6rem;font-weight:700;margin-bottom:30px}.section-desc{margin:0 0 .75rem;color:#666}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}.form.form--compact .grid{gap:12px}.col{grid-column:span var(--col-span, 6);min-width:0;width:100%;position:relative;padding-bottom:20px}.col--group-wrapper{grid-column:span 12;display:grid;grid-template-columns:repeat(4,1fr);gap:16px;padding-bottom:0}.form.form--compact .col--group-wrapper{gap:12px}.group-item--full{grid-column:span 4}.form.form--compact .col{padding-bottom:0}.label{position:absolute;top:-8px;left:12px;font-size:1.2rem;color:#454733;background-color:var(--sl-form-surface, #E3E3D1);padding:0 4px;font-weight:500;text-transform:capitalize}.input{width:100%;border:1px solid #787861;border-radius:5px;padding:15px;background-color:transparent}input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=date]::-webkit-calendar-picker-indicator{position:absolute;inset:0;width:auto;height:auto;color:transparent;background:transparent}.input:focus{outline:none;border-color:#a9a97f}.input--disabled{color:#888}.input:-webkit-autofill,.input:-webkit-autofill:hover,.input:-webkit-autofill:focus,.input:-webkit-autofill:active{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733;caret-color:#454733;transition:background-color 9999s ease-in-out 0s}.input:-moz-autofill,.textarea:-moz-autofill{box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset}.input[readonly]:-webkit-autofill,.input:disabled:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#a9a97f}.label-radio{font-size:1.4rem;position:static;padding-left:0}.radio-group,.checkbox-group{display:flex;gap:2rem;padding:.5rem 0}.form.form--compact .checkbox-group{padding:0 8px 0 0}.checkbox,.radio{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:1.4rem;color:#1c1c12;-webkit-user-select:none;user-select:none}.checkbox:has(.checkbox-subtitle){align-items:flex-start}.checkbox:has(.checkbox-subtitle) .checkbox-mark,.checkbox-group--cards .checkbox-mark{margin-top:2px}.checkbox-text{display:flex;flex-direction:column;gap:2px}.checkbox-subtitle{font-size:1.2rem;color:#787861;font-weight:400;line-height:1.3}.checkbox-label,.checkbox-subtitle{text-transform:uppercase}.checkbox-group--lowercase .checkbox-label,.checkbox-group--lowercase .checkbox-subtitle{text-transform:none}.checkbox:hover{color:#454733}.checkbox input,.radio input{position:absolute;opacity:0;pointer-events:none}.checkbox-mark{width:18px;height:18px;border:2px solid #787861;border-radius:4px;background-color:transparent;position:relative;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.checkbox input:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox input:checked+.checkbox-mark:after{content:\"\";position:absolute;width:6px;height:10px;border:solid #fff;border-width:0 2px 2px 0;transform:translate(-50%,-60%) rotate(45deg);top:50%;left:50%}.radio-mark{width:18px;height:18px;border:2px solid #787861;border-radius:50%;background-color:transparent;position:relative;transition:all .2s ease}.radio input:checked+.radio-mark{border-color:#596300}.radio input:checked+.radio-mark:after{content:\"\";width:8px;height:8px;background-color:#454733;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.radio:hover .radio-mark{border-color:#a9a97f}.checkbox input:disabled+.checkbox-mark,.radio input:disabled+.radio-mark{border-color:#a9a97f;background-color:var(--sl-form-surface, #E3E3D1);cursor:not-allowed}.checkbox:has(input:disabled),.radio:has(input:disabled){cursor:not-allowed}.checkbox:has(input:disabled) .checkbox-text,.radio:has(input:disabled) .radio-label{color:#1c1c1266}.checkbox-group--cards .checkbox:has(input:disabled){opacity:.5;pointer-events:none}.checkbox-group--readonly .checkbox{opacity:.6;pointer-events:none;cursor:not-allowed}.checkbox-group--readonly .checkbox input:disabled+.checkbox-mark{border-color:#787861;background-color:transparent}.checkbox-group--readonly .checkbox input:disabled:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox-group--cards.checkbox-group--readonly .checkbox{opacity:.8;pointer-events:none}.checkbox-group--cards .checkbox{display:flex;align-items:flex-start;width:100%;gap:12px;padding:14px 16px;background-color:#f0f0db;font-size:14px;font-weight:300;text-transform:uppercase;border-radius:10px;border:1px solid transparent;transition:all .2s ease;cursor:pointer;position:relative}.checkbox-group--cards .checkbox:hover{background-color:#dee58f}.error{position:absolute;bottom:0;left:0;font-size:1.2rem;color:#b00020;width:100%;height:15px;display:none}.col:has(.error) .error{display:block}.textarea{resize:vertical;min-height:100px;max-height:300px;line-height:1.5}.textarea:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\");background-repeat:no-repeat;background-size:15px;background-position:right 15px center;cursor:pointer}select:invalid{color:#787861}select.placeholder-selected{color:#787861}select:not(.placeholder-selected){color:#000}select option{color:#454733;cursor:pointer}input[type=date]{color:#787861!important}input[type=date]:valid{color:#454733!important}.label-disabled{color:#1c1c1266}.input:disabled,.input--disabled,.input.disabled,.input[readonly]{border:1px solid #a9a97f;color:#a9a97f;cursor:not-allowed;pointer-events:none}@media (max-width: 768px){.grid{grid-template-columns:1fr}.col{grid-column:span 1!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: SelectCustomSearch, selector: "lib-select-custom-search", inputs: ["options", "placeholder", "readonly"], outputs: ["selectionChange"] }, { kind: "component", type: DateTimePicker, selector: "lib-date-time-picker", inputs: ["mode", "placeholder", "minDate", "maxDate", "readonly"], outputs: ["dateChange"] }, { kind: "component", type: ToggleCustom, selector: "lib-toggle-custom", inputs: ["label", "key", "disabled", "checked"], outputs: ["checkedChange", "toggleChange"] }] });
2794
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: DynamicFormFields, isStandalone: true, selector: "lib-dynamic-form-fields", inputs: { form: { classPropertyName: "form", publicName: "form", isSignal: true, isRequired: true, transformFunction: null }, sections: { classPropertyName: "sections", publicName: "sections", isSignal: true, isRequired: false, transformFunction: null }, compact: { classPropertyName: "compact", publicName: "compact", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "@if (form()) {\n<form class=\"form\" [formGroup]=\"form()\" [class.form--compact]=\"compact()\" (keydown)=\"onEnter($event)\">\n @for (sec of sections(); track $index) {\n <section class=\"section\">\n @if (sec.title) {\n <h3 class=\"section-title\">{{ sec.title }}</h3>\n } @if (sec.description) {\n <p class=\"section-desc\">{{ sec.description }}</p>\n }\n\n <div class=\"grid\">\n @for (run of getFieldRuns(sec.fields); track $index) {\n\n @if (run.isGroup) {\n <!-- toggle: wrapper que siempre ocupa la fila completa -->\n <div class=\"col col--group-wrapper\">\n @for (f of run.fields; track f.key) {\n <div class=\"group-item\">\n <lib-toggle-custom [key]=\"f.key\" [label]=\"f.label ?? ''\" [formControlName]=\"f.key\" />\n </div>\n }\n </div>\n\n } @else {\n <!-- Campos normales (incluye checkbox y radio con su col configurable) -->\n @for (f of run.fields; track f.key) {\n <div class=\"col\" [style.--col-span]=\"f.col || 6\">\n @if (['text','number','email', 'password', 'time'].includes(f.type))\n {\n <input\n class=\"input\"\n [type]=\"f.type\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n (input)=\"onUppercaseInput($event, f.type, f.key)\"\n />\n } @if (['date', 'datetime-local'].includes(f.type)) {\n <lib-date-time-picker\n [mode]=\"getDatePickerMode(f.type)\"\n [placeholder]=\"\n f.placeholder ||\n (f.type === 'date'\n ? 'Seleccionar fecha'\n : 'Seleccionar fecha y hora')\n \"\n [formControlName]=\"f.key\"\n [minDate]=\"f.minDate || null\"\n [maxDate]=\"f.maxDate || null\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'textarea') {\n <textarea\n class=\"input textarea\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n rows=\"6\"\n [readonly]=\"f.readonly || false\"\n ></textarea>\n } @if (f.type === 'select') {\n <lib-select-custom-search\n [options]=\"f.options ?? []\"\n [placeholder]=\"f.placeholder || 'Seleccionar...'\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'radio') {\n <div class=\"radio-group\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"radio\">\n <input\n type=\"radio\"\n [value]=\"o.value\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n <span class=\"radio-mark\"></span>\n <span class=\"radio-label\">{{ o.label }}</span>\n </label>\n }\n </div>\n } @if (f.type === 'checkbox') {\n <div class=\"checkbox-group\"\n [class.checkbox-group--cards]=\"f.variant === 'cards'\"\n [class.checkbox-group--lowercase]=\"f.uppercase === false\"\n [class.checkbox-group--readonly]=\"f.readonly && !f.disabled\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"getCheckboxArray(f.key)?.at($index)?.value\"\n (change)=\"onCheckboxInteract($event, f, $index)\"\n [disabled]=\"f.disabled || f.readonly || false\"\n />\n <span class=\"checkbox-mark\"></span>\n <span class=\"checkbox-text\">\n <span class=\"checkbox-label\">{{ o.label }}</span>\n @if (o.subtitle) {\n <span class=\"checkbox-subtitle\">{{ o.subtitle }}</span>\n }\n </span>\n </label>\n }\n </div>\n } @if (f.type === 'disabled') {\n <input\n class=\"input input--disabled\"\n [placeholder]=\"f.placeholder || 'Autom\u00E1tico'\"\n disabled\n />\n } @if (ctrl(f.key)?.touched && ctrl(f.key)?.invalid) {\n <div class=\"error\">\n @if (ctrl(f.key)?.errors?.['required']) {\n <span>Campo requerido</span>\n } @if (ctrl(f.key)?.errors?.['email']) {\n <span>Correo inv\u00E1lido</span>\n } @if (ctrl(f.key)?.errors?.['pattern']) {\n <span>\n @switch (f.patternType) { @case ('numbers') { Solo se permiten\n n\u00FAmeros } @case ('phone') { Formato de tel\u00E9fono inv\u00E1lido } @case\n ('text') { Solo se permiten letras y espacios } @case ('username') {\n Solo se permiten letras, n\u00FAmeros, puntos y guiones bajos (no al\n inicio ni al final) } @case ('alphanumeric') { Solo se permiten\n letras y n\u00FAmeros } @default { Formato inv\u00E1lido } }\n </span>\n } @if (ctrl(f.key)?.errors?.['notMatching']) {\n <span>Las contrase\u00F1as no coinciden</span>\n }\n </div>\n }\n\n @if(f.label && f.type !== 'toggle') {\n <label\n [class.label-radio]=\"f.type === 'radio'\"\n [class.label-checkbox]=\"f.type === 'checkbox'\"\n [class.label-disabled]=\"f.readonly || f.disabled\"\n class=\"label\"\n >\n {{ f.label }} </label>\n }\n </div>\n }\n }\n\n }\n </div>\n </section>\n }\n</form>\n}\n", styles: [".form{width:100%}.form{display:grid}.section{padding:20px 0 0}.section-title{font-size:1.6rem;font-weight:700;margin-bottom:30px}.section-desc{margin:0 0 .75rem;color:#666}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}.form.form--compact .grid{gap:12px}.col{grid-column:span var(--col-span, 6);min-width:0;width:100%;position:relative;padding-bottom:20px}.col--group-wrapper{grid-column:span 12;display:grid;grid-template-columns:repeat(4,1fr);gap:16px;padding-bottom:0}.form.form--compact .col--group-wrapper{gap:12px}.group-item--full{grid-column:span 4}.form.form--compact .col{padding-bottom:0}.label{position:absolute;top:-8px;left:12px;font-size:1.2rem;color:#454733;background-color:var(--sl-form-surface, #E3E3D1);padding:0 4px;font-weight:500;text-transform:capitalize}.input{width:100%;border:1px solid #787861;border-radius:5px;padding:15px;background-color:transparent}input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=date]::-webkit-calendar-picker-indicator{position:absolute;inset:0;width:auto;height:auto;color:transparent;background:transparent}.input:focus{outline:none;border-color:#a9a97f}.input--disabled{color:#888}.input:-webkit-autofill,.input:-webkit-autofill:hover,.input:-webkit-autofill:focus,.input:-webkit-autofill:active{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733;caret-color:#454733;transition:background-color 9999s ease-in-out 0s}.input:-moz-autofill,.textarea:-moz-autofill{box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset}.input[readonly]:-webkit-autofill,.input:disabled:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#a9a97f}.label-radio{font-size:1.4rem;position:static;padding-left:0}.radio-group,.checkbox-group{display:flex;gap:2rem;padding:.5rem 0}.form.form--compact .checkbox-group{padding:0 8px 0 0}.checkbox,.radio{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:1.4rem;color:#1c1c12;-webkit-user-select:none;user-select:none}.checkbox:has(.checkbox-subtitle){align-items:flex-start}.checkbox:has(.checkbox-subtitle) .checkbox-mark,.checkbox-group--cards .checkbox-mark{margin-top:2px}.checkbox-text{display:flex;flex-direction:column;gap:2px}.checkbox-subtitle{font-size:1.2rem;color:#787861;font-weight:400;line-height:1.3}.checkbox-label,.checkbox-subtitle{text-transform:uppercase}.checkbox-group--lowercase .checkbox-label,.checkbox-group--lowercase .checkbox-subtitle{text-transform:none}.checkbox:hover{color:#454733}.checkbox input,.radio input{position:absolute;opacity:0;pointer-events:none}.checkbox-mark{width:18px;height:18px;border:2px solid #787861;border-radius:4px;background-color:transparent;position:relative;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.checkbox input:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox input:checked+.checkbox-mark:after{content:\"\";position:absolute;width:6px;height:10px;border:solid #fff;border-width:0 2px 2px 0;transform:translate(-50%,-60%) rotate(45deg);top:50%;left:50%}.radio-mark{width:18px;height:18px;border:2px solid #787861;border-radius:50%;background-color:transparent;position:relative;transition:all .2s ease}.radio input:checked+.radio-mark{border-color:#596300}.radio input:checked+.radio-mark:after{content:\"\";width:8px;height:8px;background-color:#454733;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.radio:hover .radio-mark{border-color:#a9a97f}.checkbox input:disabled+.checkbox-mark,.radio input:disabled+.radio-mark{border-color:#a9a97f;background-color:var(--sl-form-surface, #E3E3D1);cursor:not-allowed}.checkbox:has(input:disabled),.radio:has(input:disabled){cursor:not-allowed}.checkbox:has(input:disabled) .checkbox-text,.radio:has(input:disabled) .radio-label{color:#1c1c1266}.checkbox-group--cards .checkbox:has(input:disabled){opacity:.5;pointer-events:none}.checkbox-group--readonly .checkbox{opacity:.6;pointer-events:none;cursor:not-allowed}.checkbox-group--readonly .checkbox input:disabled+.checkbox-mark{border-color:#787861;background-color:transparent}.checkbox-group--readonly .checkbox input:disabled:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox-group--cards.checkbox-group--readonly .checkbox{opacity:.8;pointer-events:none}.checkbox-group--cards .checkbox{display:flex;align-items:flex-start;width:100%;gap:12px;padding:14px 16px;background-color:#f0f0db;font-size:14px;font-weight:300;text-transform:uppercase;border-radius:10px;border:1px solid transparent;transition:all .2s ease;cursor:pointer;position:relative}.checkbox-group--cards .checkbox:hover{background-color:#dee58f}.error{position:absolute;bottom:0;left:0;font-size:1.2rem;color:#b00020;width:100%;height:15px;display:none}.col:has(.error) .error{display:block}.textarea{resize:vertical;min-height:100px;max-height:300px;line-height:1.5;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#a9a97f transparent}.textarea::-webkit-scrollbar{width:8px}.textarea::-webkit-scrollbar-track{background:transparent;margin:4px 0}.textarea::-webkit-scrollbar-thumb{background:#a9a97f;border-radius:4px}.textarea::-webkit-scrollbar-thumb:hover{background:#787861}.textarea:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\");background-repeat:no-repeat;background-size:15px;background-position:right 15px center;cursor:pointer}select:invalid{color:#787861}select.placeholder-selected{color:#787861}select:not(.placeholder-selected){color:#000}select option{color:#454733;cursor:pointer}input[type=date]{color:#787861!important}input[type=date]:valid{color:#454733!important}.label-disabled{color:#1c1c1266}.input:disabled,.input--disabled,.input.disabled,.input[readonly]{border:1px solid #a9a97f;color:#a9a97f;cursor:not-allowed;pointer-events:none}@media (max-width: 768px){.grid{grid-template-columns:1fr}.col{grid-column:span 1!important}}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: SelectCustomSearch, selector: "lib-select-custom-search", inputs: ["options", "placeholder", "readonly"], outputs: ["selectionChange"] }, { kind: "component", type: DateTimePicker, selector: "lib-date-time-picker", inputs: ["mode", "placeholder", "minDate", "maxDate", "readonly"], outputs: ["dateChange"] }, { kind: "component", type: ToggleCustom, selector: "lib-toggle-custom", inputs: ["label", "key", "disabled", "checked"], outputs: ["checkedChange", "toggleChange"] }] });
2721
2795
  }
2722
2796
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DynamicFormFields, decorators: [{
2723
2797
  type: Component,
2724
- args: [{ selector: 'lib-dynamic-form-fields', standalone: true, imports: [ReactiveFormsModule, SelectCustomSearch, DateTimePicker, ToggleCustom], template: "@if (form()) {\n<form class=\"form\" [formGroup]=\"form()\" [class.form--compact]=\"compact()\" (keydown)=\"onEnter($event)\">\n @for (sec of sections(); track $index) {\n <section class=\"section\">\n @if (sec.title) {\n <h3 class=\"section-title\">{{ sec.title }}</h3>\n } @if (sec.description) {\n <p class=\"section-desc\">{{ sec.description }}</p>\n }\n\n <div class=\"grid\">\n @for (run of getFieldRuns(sec.fields); track $index) {\n\n @if (run.isGroup) {\n <!-- toggle: wrapper que siempre ocupa la fila completa -->\n <div class=\"col col--group-wrapper\">\n @for (f of run.fields; track f.key) {\n <div class=\"group-item\">\n <lib-toggle-custom [key]=\"f.key\" [label]=\"f.label ?? ''\" [formControlName]=\"f.key\" />\n </div>\n }\n </div>\n\n } @else {\n <!-- Campos normales (incluye checkbox y radio con su col configurable) -->\n @for (f of run.fields; track f.key) {\n <div class=\"col\" [style.--col-span]=\"f.col || 6\">\n @if (['text','number','email', 'password', 'time'].includes(f.type))\n {\n <input\n class=\"input\"\n [type]=\"f.type\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n (input)=\"onUppercaseInput($event, f.type, f.key)\"\n />\n } @if (['date', 'datetime-local'].includes(f.type)) {\n <lib-date-time-picker\n [mode]=\"getDatePickerMode(f.type)\"\n [placeholder]=\"\n f.placeholder ||\n (f.type === 'date'\n ? 'Seleccionar fecha'\n : 'Seleccionar fecha y hora')\n \"\n [formControlName]=\"f.key\"\n [minDate]=\"f.minDate || null\"\n [maxDate]=\"f.maxDate || null\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'textarea') {\n <textarea\n class=\"input textarea\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n rows=\"6\"\n [readonly]=\"f.readonly || false\"\n ></textarea>\n } @if (f.type === 'select') {\n <lib-select-custom-search\n [options]=\"f.options ?? []\"\n [placeholder]=\"f.placeholder || 'Seleccionar...'\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'radio') {\n <div class=\"radio-group\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"radio\">\n <input\n type=\"radio\"\n [value]=\"o.value\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n <span class=\"radio-mark\"></span>\n <span class=\"radio-label\">{{ o.label }}</span>\n </label>\n }\n </div>\n } @if (f.type === 'checkbox') {\n <div class=\"checkbox-group\"\n [class.checkbox-group--cards]=\"f.variant === 'cards'\"\n [class.checkbox-group--lowercase]=\"f.uppercase === false\"\n [class.checkbox-group--readonly]=\"f.readonly && !f.disabled\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"getCheckboxArray(f.key)?.at($index)?.value\"\n (change)=\"onCheckboxInteract($event, f, $index)\"\n [disabled]=\"f.disabled || f.readonly || false\"\n />\n <span class=\"checkbox-mark\"></span>\n <span class=\"checkbox-text\">\n <span class=\"checkbox-label\">{{ o.label }}</span>\n @if (o.subtitle) {\n <span class=\"checkbox-subtitle\">{{ o.subtitle }}</span>\n }\n </span>\n </label>\n }\n </div>\n } @if (f.type === 'disabled') {\n <input\n class=\"input input--disabled\"\n [placeholder]=\"f.placeholder || 'Autom\u00E1tico'\"\n disabled\n />\n } @if (ctrl(f.key)?.touched && ctrl(f.key)?.invalid) {\n <div class=\"error\">\n @if (ctrl(f.key)?.errors?.['required']) {\n <span>Campo requerido</span>\n } @if (ctrl(f.key)?.errors?.['email']) {\n <span>Correo inv\u00E1lido</span>\n } @if (ctrl(f.key)?.errors?.['pattern']) {\n <span>\n @switch (f.patternType) { @case ('numbers') { Solo se permiten\n n\u00FAmeros } @case ('phone') { Formato de tel\u00E9fono inv\u00E1lido } @case\n ('text') { Solo se permiten letras y espacios } @case ('username') {\n Solo se permiten letras, n\u00FAmeros, puntos y guiones bajos (no al\n inicio ni al final) } @case ('alphanumeric') { Solo se permiten\n letras y n\u00FAmeros } @default { Formato inv\u00E1lido } }\n </span>\n } @if (ctrl(f.key)?.errors?.['notMatching']) {\n <span>Las contrase\u00F1as no coinciden</span>\n }\n </div>\n }\n\n @if(f.label && f.type !== 'toggle') {\n <label\n [class.label-radio]=\"f.type === 'radio'\"\n [class.label-checkbox]=\"f.type === 'checkbox'\"\n [class.label-disabled]=\"f.readonly || f.disabled\"\n class=\"label\"\n >\n {{ f.label }} </label>\n }\n </div>\n }\n }\n\n }\n </div>\n </section>\n }\n</form>\n}\n", styles: [".form{width:100%}.form{display:grid}.section{padding:20px 0 0}.section-title{font-size:1.6rem;font-weight:700;margin-bottom:30px}.section-desc{margin:0 0 .75rem;color:#666}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}.form.form--compact .grid{gap:12px}.col{grid-column:span var(--col-span, 6);min-width:0;width:100%;position:relative;padding-bottom:20px}.col--group-wrapper{grid-column:span 12;display:grid;grid-template-columns:repeat(4,1fr);gap:16px;padding-bottom:0}.form.form--compact .col--group-wrapper{gap:12px}.group-item--full{grid-column:span 4}.form.form--compact .col{padding-bottom:0}.label{position:absolute;top:-8px;left:12px;font-size:1.2rem;color:#454733;background-color:var(--sl-form-surface, #E3E3D1);padding:0 4px;font-weight:500;text-transform:capitalize}.input{width:100%;border:1px solid #787861;border-radius:5px;padding:15px;background-color:transparent}input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=date]::-webkit-calendar-picker-indicator{position:absolute;inset:0;width:auto;height:auto;color:transparent;background:transparent}.input:focus{outline:none;border-color:#a9a97f}.input--disabled{color:#888}.input:-webkit-autofill,.input:-webkit-autofill:hover,.input:-webkit-autofill:focus,.input:-webkit-autofill:active{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733;caret-color:#454733;transition:background-color 9999s ease-in-out 0s}.input:-moz-autofill,.textarea:-moz-autofill{box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset}.input[readonly]:-webkit-autofill,.input:disabled:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#a9a97f}.label-radio{font-size:1.4rem;position:static;padding-left:0}.radio-group,.checkbox-group{display:flex;gap:2rem;padding:.5rem 0}.form.form--compact .checkbox-group{padding:0 8px 0 0}.checkbox,.radio{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:1.4rem;color:#1c1c12;-webkit-user-select:none;user-select:none}.checkbox:has(.checkbox-subtitle){align-items:flex-start}.checkbox:has(.checkbox-subtitle) .checkbox-mark,.checkbox-group--cards .checkbox-mark{margin-top:2px}.checkbox-text{display:flex;flex-direction:column;gap:2px}.checkbox-subtitle{font-size:1.2rem;color:#787861;font-weight:400;line-height:1.3}.checkbox-label,.checkbox-subtitle{text-transform:uppercase}.checkbox-group--lowercase .checkbox-label,.checkbox-group--lowercase .checkbox-subtitle{text-transform:none}.checkbox:hover{color:#454733}.checkbox input,.radio input{position:absolute;opacity:0;pointer-events:none}.checkbox-mark{width:18px;height:18px;border:2px solid #787861;border-radius:4px;background-color:transparent;position:relative;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.checkbox input:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox input:checked+.checkbox-mark:after{content:\"\";position:absolute;width:6px;height:10px;border:solid #fff;border-width:0 2px 2px 0;transform:translate(-50%,-60%) rotate(45deg);top:50%;left:50%}.radio-mark{width:18px;height:18px;border:2px solid #787861;border-radius:50%;background-color:transparent;position:relative;transition:all .2s ease}.radio input:checked+.radio-mark{border-color:#596300}.radio input:checked+.radio-mark:after{content:\"\";width:8px;height:8px;background-color:#454733;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.radio:hover .radio-mark{border-color:#a9a97f}.checkbox input:disabled+.checkbox-mark,.radio input:disabled+.radio-mark{border-color:#a9a97f;background-color:var(--sl-form-surface, #E3E3D1);cursor:not-allowed}.checkbox:has(input:disabled),.radio:has(input:disabled){cursor:not-allowed}.checkbox:has(input:disabled) .checkbox-text,.radio:has(input:disabled) .radio-label{color:#1c1c1266}.checkbox-group--cards .checkbox:has(input:disabled){opacity:.5;pointer-events:none}.checkbox-group--readonly .checkbox{opacity:.6;pointer-events:none;cursor:not-allowed}.checkbox-group--readonly .checkbox input:disabled+.checkbox-mark{border-color:#787861;background-color:transparent}.checkbox-group--readonly .checkbox input:disabled:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox-group--cards.checkbox-group--readonly .checkbox{opacity:.8;pointer-events:none}.checkbox-group--cards .checkbox{display:flex;align-items:flex-start;width:100%;gap:12px;padding:14px 16px;background-color:#f0f0db;font-size:14px;font-weight:300;text-transform:uppercase;border-radius:10px;border:1px solid transparent;transition:all .2s ease;cursor:pointer;position:relative}.checkbox-group--cards .checkbox:hover{background-color:#dee58f}.error{position:absolute;bottom:0;left:0;font-size:1.2rem;color:#b00020;width:100%;height:15px;display:none}.col:has(.error) .error{display:block}.textarea{resize:vertical;min-height:100px;max-height:300px;line-height:1.5}.textarea:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\");background-repeat:no-repeat;background-size:15px;background-position:right 15px center;cursor:pointer}select:invalid{color:#787861}select.placeholder-selected{color:#787861}select:not(.placeholder-selected){color:#000}select option{color:#454733;cursor:pointer}input[type=date]{color:#787861!important}input[type=date]:valid{color:#454733!important}.label-disabled{color:#1c1c1266}.input:disabled,.input--disabled,.input.disabled,.input[readonly]{border:1px solid #a9a97f;color:#a9a97f;cursor:not-allowed;pointer-events:none}@media (max-width: 768px){.grid{grid-template-columns:1fr}.col{grid-column:span 1!important}}\n"] }]
2798
+ args: [{ selector: 'lib-dynamic-form-fields', standalone: true, imports: [ReactiveFormsModule, SelectCustomSearch, DateTimePicker, ToggleCustom], template: "@if (form()) {\n<form class=\"form\" [formGroup]=\"form()\" [class.form--compact]=\"compact()\" (keydown)=\"onEnter($event)\">\n @for (sec of sections(); track $index) {\n <section class=\"section\">\n @if (sec.title) {\n <h3 class=\"section-title\">{{ sec.title }}</h3>\n } @if (sec.description) {\n <p class=\"section-desc\">{{ sec.description }}</p>\n }\n\n <div class=\"grid\">\n @for (run of getFieldRuns(sec.fields); track $index) {\n\n @if (run.isGroup) {\n <!-- toggle: wrapper que siempre ocupa la fila completa -->\n <div class=\"col col--group-wrapper\">\n @for (f of run.fields; track f.key) {\n <div class=\"group-item\">\n <lib-toggle-custom [key]=\"f.key\" [label]=\"f.label ?? ''\" [formControlName]=\"f.key\" />\n </div>\n }\n </div>\n\n } @else {\n <!-- Campos normales (incluye checkbox y radio con su col configurable) -->\n @for (f of run.fields; track f.key) {\n <div class=\"col\" [style.--col-span]=\"f.col || 6\">\n @if (['text','number','email', 'password', 'time'].includes(f.type))\n {\n <input\n class=\"input\"\n [type]=\"f.type\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n (input)=\"onUppercaseInput($event, f.type, f.key)\"\n />\n } @if (['date', 'datetime-local'].includes(f.type)) {\n <lib-date-time-picker\n [mode]=\"getDatePickerMode(f.type)\"\n [placeholder]=\"\n f.placeholder ||\n (f.type === 'date'\n ? 'Seleccionar fecha'\n : 'Seleccionar fecha y hora')\n \"\n [formControlName]=\"f.key\"\n [minDate]=\"f.minDate || null\"\n [maxDate]=\"f.maxDate || null\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'textarea') {\n <textarea\n class=\"input textarea\"\n [placeholder]=\"f.placeholder\"\n [formControlName]=\"f.key\"\n rows=\"6\"\n [readonly]=\"f.readonly || false\"\n ></textarea>\n } @if (f.type === 'select') {\n <lib-select-custom-search\n [options]=\"f.options ?? []\"\n [placeholder]=\"f.placeholder || 'Seleccionar...'\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n } @if (f.type === 'radio') {\n <div class=\"radio-group\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"radio\">\n <input\n type=\"radio\"\n [value]=\"o.value\"\n [formControlName]=\"f.key\"\n [readonly]=\"f.readonly || false\"\n />\n <span class=\"radio-mark\"></span>\n <span class=\"radio-label\">{{ o.label }}</span>\n </label>\n }\n </div>\n } @if (f.type === 'checkbox') {\n <div class=\"checkbox-group\"\n [class.checkbox-group--cards]=\"f.variant === 'cards'\"\n [class.checkbox-group--lowercase]=\"f.uppercase === false\"\n [class.checkbox-group--readonly]=\"f.readonly && !f.disabled\">\n @for (o of f.options ?? []; track o.value) {\n <label class=\"checkbox\">\n <input\n type=\"checkbox\"\n [checked]=\"getCheckboxArray(f.key)?.at($index)?.value\"\n (change)=\"onCheckboxInteract($event, f, $index)\"\n [disabled]=\"f.disabled || f.readonly || false\"\n />\n <span class=\"checkbox-mark\"></span>\n <span class=\"checkbox-text\">\n <span class=\"checkbox-label\">{{ o.label }}</span>\n @if (o.subtitle) {\n <span class=\"checkbox-subtitle\">{{ o.subtitle }}</span>\n }\n </span>\n </label>\n }\n </div>\n } @if (f.type === 'disabled') {\n <input\n class=\"input input--disabled\"\n [placeholder]=\"f.placeholder || 'Autom\u00E1tico'\"\n disabled\n />\n } @if (ctrl(f.key)?.touched && ctrl(f.key)?.invalid) {\n <div class=\"error\">\n @if (ctrl(f.key)?.errors?.['required']) {\n <span>Campo requerido</span>\n } @if (ctrl(f.key)?.errors?.['email']) {\n <span>Correo inv\u00E1lido</span>\n } @if (ctrl(f.key)?.errors?.['pattern']) {\n <span>\n @switch (f.patternType) { @case ('numbers') { Solo se permiten\n n\u00FAmeros } @case ('phone') { Formato de tel\u00E9fono inv\u00E1lido } @case\n ('text') { Solo se permiten letras y espacios } @case ('username') {\n Solo se permiten letras, n\u00FAmeros, puntos y guiones bajos (no al\n inicio ni al final) } @case ('alphanumeric') { Solo se permiten\n letras y n\u00FAmeros } @default { Formato inv\u00E1lido } }\n </span>\n } @if (ctrl(f.key)?.errors?.['notMatching']) {\n <span>Las contrase\u00F1as no coinciden</span>\n }\n </div>\n }\n\n @if(f.label && f.type !== 'toggle') {\n <label\n [class.label-radio]=\"f.type === 'radio'\"\n [class.label-checkbox]=\"f.type === 'checkbox'\"\n [class.label-disabled]=\"f.readonly || f.disabled\"\n class=\"label\"\n >\n {{ f.label }} </label>\n }\n </div>\n }\n }\n\n }\n </div>\n </section>\n }\n</form>\n}\n", styles: [".form{width:100%}.form{display:grid}.section{padding:20px 0 0}.section-title{font-size:1.6rem;font-weight:700;margin-bottom:30px}.section-desc{margin:0 0 .75rem;color:#666}.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}.form.form--compact .grid{gap:12px}.col{grid-column:span var(--col-span, 6);min-width:0;width:100%;position:relative;padding-bottom:20px}.col--group-wrapper{grid-column:span 12;display:grid;grid-template-columns:repeat(4,1fr);gap:16px;padding-bottom:0}.form.form--compact .col--group-wrapper{gap:12px}.group-item--full{grid-column:span 4}.form.form--compact .col{padding-bottom:0}.label{position:absolute;top:-8px;left:12px;font-size:1.2rem;color:#454733;background-color:var(--sl-form-surface, #E3E3D1);padding:0 4px;font-weight:500;text-transform:capitalize}.input{width:100%;border:1px solid #787861;border-radius:5px;padding:15px;background-color:transparent}input[type=number]::-webkit-outer-spin-button,input[type=number]::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}input[type=date]::-webkit-calendar-picker-indicator{position:absolute;inset:0;width:auto;height:auto;color:transparent;background:transparent}.input:focus{outline:none;border-color:#a9a97f}.input--disabled{color:#888}.input:-webkit-autofill,.input:-webkit-autofill:hover,.input:-webkit-autofill:focus,.input:-webkit-autofill:active{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733;caret-color:#454733;transition:background-color 9999s ease-in-out 0s}.input:-moz-autofill,.textarea:-moz-autofill{box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset}.input[readonly]:-webkit-autofill,.input:disabled:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#a9a97f}.label-radio{font-size:1.4rem;position:static;padding-left:0}.radio-group,.checkbox-group{display:flex;gap:2rem;padding:.5rem 0}.form.form--compact .checkbox-group{padding:0 8px 0 0}.checkbox,.radio{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:1.4rem;color:#1c1c12;-webkit-user-select:none;user-select:none}.checkbox:has(.checkbox-subtitle){align-items:flex-start}.checkbox:has(.checkbox-subtitle) .checkbox-mark,.checkbox-group--cards .checkbox-mark{margin-top:2px}.checkbox-text{display:flex;flex-direction:column;gap:2px}.checkbox-subtitle{font-size:1.2rem;color:#787861;font-weight:400;line-height:1.3}.checkbox-label,.checkbox-subtitle{text-transform:uppercase}.checkbox-group--lowercase .checkbox-label,.checkbox-group--lowercase .checkbox-subtitle{text-transform:none}.checkbox:hover{color:#454733}.checkbox input,.radio input{position:absolute;opacity:0;pointer-events:none}.checkbox-mark{width:18px;height:18px;border:2px solid #787861;border-radius:4px;background-color:transparent;position:relative;display:inline-flex;align-items:center;justify-content:center;transition:all .2s ease}.checkbox input:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox input:checked+.checkbox-mark:after{content:\"\";position:absolute;width:6px;height:10px;border:solid #fff;border-width:0 2px 2px 0;transform:translate(-50%,-60%) rotate(45deg);top:50%;left:50%}.radio-mark{width:18px;height:18px;border:2px solid #787861;border-radius:50%;background-color:transparent;position:relative;transition:all .2s ease}.radio input:checked+.radio-mark{border-color:#596300}.radio input:checked+.radio-mark:after{content:\"\";width:8px;height:8px;background-color:#454733;border-radius:50%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.radio:hover .radio-mark{border-color:#a9a97f}.checkbox input:disabled+.checkbox-mark,.radio input:disabled+.radio-mark{border-color:#a9a97f;background-color:var(--sl-form-surface, #E3E3D1);cursor:not-allowed}.checkbox:has(input:disabled),.radio:has(input:disabled){cursor:not-allowed}.checkbox:has(input:disabled) .checkbox-text,.radio:has(input:disabled) .radio-label{color:#1c1c1266}.checkbox-group--cards .checkbox:has(input:disabled){opacity:.5;pointer-events:none}.checkbox-group--readonly .checkbox{opacity:.6;pointer-events:none;cursor:not-allowed}.checkbox-group--readonly .checkbox input:disabled+.checkbox-mark{border-color:#787861;background-color:transparent}.checkbox-group--readonly .checkbox input:disabled:checked+.checkbox-mark{background-color:#596300;border-color:#596300}.checkbox-group--cards.checkbox-group--readonly .checkbox{opacity:.8;pointer-events:none}.checkbox-group--cards .checkbox{display:flex;align-items:flex-start;width:100%;gap:12px;padding:14px 16px;background-color:#f0f0db;font-size:14px;font-weight:300;text-transform:uppercase;border-radius:10px;border:1px solid transparent;transition:all .2s ease;cursor:pointer;position:relative}.checkbox-group--cards .checkbox:hover{background-color:#dee58f}.error{position:absolute;bottom:0;left:0;font-size:1.2rem;color:#b00020;width:100%;height:15px;display:none}.col:has(.error) .error{display:block}.textarea{resize:vertical;min-height:100px;max-height:300px;line-height:1.5;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#a9a97f transparent}.textarea::-webkit-scrollbar{width:8px}.textarea::-webkit-scrollbar-track{background:transparent;margin:4px 0}.textarea::-webkit-scrollbar-thumb{background:#a9a97f;border-radius:4px}.textarea::-webkit-scrollbar-thumb:hover{background:#787861}.textarea:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px var(--sl-form-surface, #E3E3D1) inset;-webkit-text-fill-color:#454733}select{appearance:none;-webkit-appearance:none;-moz-appearance:none;background-image:url(\"data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e\");background-repeat:no-repeat;background-size:15px;background-position:right 15px center;cursor:pointer}select:invalid{color:#787861}select.placeholder-selected{color:#787861}select:not(.placeholder-selected){color:#000}select option{color:#454733;cursor:pointer}input[type=date]{color:#787861!important}input[type=date]:valid{color:#454733!important}.label-disabled{color:#1c1c1266}.input:disabled,.input--disabled,.input.disabled,.input[readonly]{border:1px solid #a9a97f;color:#a9a97f;cursor:not-allowed;pointer-events:none}@media (max-width: 768px){.grid{grid-template-columns:1fr}.col{grid-column:span 1!important}}\n"] }]
2725
2799
  }], propDecorators: { form: [{ type: i0.Input, args: [{ isSignal: true, alias: "form", required: true }] }], sections: [{ type: i0.Input, args: [{ isSignal: true, alias: "sections", required: false }] }], compact: [{ type: i0.Input, args: [{ isSignal: true, alias: "compact", required: false }] }] } });
2726
2800
 
2727
2801
  class WizardForm {
@@ -2973,6 +3047,7 @@ class DialogAlertComponent {
2973
3047
  key: "observations",
2974
3048
  label: "Observaciones",
2975
3049
  type: "textarea",
3050
+ readonly: true,
2976
3051
  placeholder: "Escriba las observaciones...",
2977
3052
  col: 12,
2978
3053
  }
@@ -4638,7 +4713,8 @@ echarts.use([
4638
4713
  GraphicComponent,
4639
4714
  DataZoomComponent,
4640
4715
  VisualMapComponent,
4641
- LegendComponent
4716
+ LegendComponent,
4717
+ TitleComponent
4642
4718
  ]);
4643
4719
  const UI_CHART_TOKENS = {
4644
4720
  fontFamily: 'Inter, sans-serif',
@@ -4682,20 +4758,25 @@ const UI_CHART_TOKENS = {
4682
4758
  class BaseChart {
4683
4759
  options = input.required(...(ngDevMode ? [{ debugName: "options" }] : []));
4684
4760
  chartClick = output();
4761
+ chartInit = output();
4685
4762
  onChartClick(event) {
4686
4763
  this.chartClick.emit(event);
4687
4764
  }
4765
+ onChartInit(chart) {
4766
+ this.chartInit.emit(chart);
4767
+ }
4688
4768
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BaseChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
4689
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: BaseChart, isStandalone: true, selector: "lib-base-chart", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { chartClick: "chartClick" }, providers: [provideEchartsCore({ echarts })], ngImport: i0, template: "<div echarts class=\"ui-base-chart\" [options]=\"options()\" [autoResize]=\"true\" (chartClick)=\"onChartClick($event)\">\r\n</div>", styles: [".ui-base-chart{display:block;width:100%;height:100%;min-height:320px}\n"], dependencies: [{ kind: "directive", type: NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4769
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: BaseChart, isStandalone: true, selector: "lib-base-chart", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { chartClick: "chartClick", chartInit: "chartInit" }, providers: [provideEchartsCore({ echarts })], ngImport: i0, template: "<div echarts class=\"ui-base-chart\" [options]=\"options()\" [autoResize]=\"true\" (chartClick)=\"onChartClick($event)\" (chartInit)=\"onChartInit($event)\">\r\n</div>", styles: [".ui-base-chart{display:block;width:100%;height:100%;min-height:320px}\n"], dependencies: [{ kind: "directive", type: NgxEchartsDirective, selector: "echarts, [echarts]", inputs: ["options", "theme", "initOpts", "merge", "autoResize", "loading", "loadingType", "loadingOpts"], outputs: ["chartInit", "optionsError", "chartClick", "chartDblClick", "chartMouseDown", "chartMouseMove", "chartMouseUp", "chartMouseOver", "chartMouseOut", "chartGlobalOut", "chartContextMenu", "chartHighlight", "chartDownplay", "chartSelectChanged", "chartLegendSelectChanged", "chartLegendSelected", "chartLegendUnselected", "chartLegendLegendSelectAll", "chartLegendLegendInverseSelect", "chartLegendScroll", "chartDataZoom", "chartDataRangeSelected", "chartGraphRoam", "chartGeoRoam", "chartTreeRoam", "chartTimelineChanged", "chartTimelinePlayChanged", "chartRestore", "chartDataViewChanged", "chartMagicTypeChanged", "chartGeoSelectChanged", "chartGeoSelected", "chartGeoUnselected", "chartAxisAreaSelected", "chartBrush", "chartBrushEnd", "chartBrushSelected", "chartGlobalCursorTaken", "chartRendered", "chartFinished"], exportAs: ["echarts"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
4690
4770
  }
4691
4771
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BaseChart, decorators: [{
4692
4772
  type: Component,
4693
- args: [{ selector: 'lib-base-chart', imports: [NgxEchartsDirective], providers: [provideEchartsCore({ echarts })], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div echarts class=\"ui-base-chart\" [options]=\"options()\" [autoResize]=\"true\" (chartClick)=\"onChartClick($event)\">\r\n</div>", styles: [".ui-base-chart{display:block;width:100%;height:100%;min-height:320px}\n"] }]
4694
- }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], chartClick: [{ type: i0.Output, args: ["chartClick"] }] } });
4773
+ args: [{ selector: 'lib-base-chart', imports: [NgxEchartsDirective], providers: [provideEchartsCore({ echarts })], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div echarts class=\"ui-base-chart\" [options]=\"options()\" [autoResize]=\"true\" (chartClick)=\"onChartClick($event)\" (chartInit)=\"onChartInit($event)\">\r\n</div>", styles: [".ui-base-chart{display:block;width:100%;height:100%;min-height:320px}\n"] }]
4774
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], chartClick: [{ type: i0.Output, args: ["chartClick"] }], chartInit: [{ type: i0.Output, args: ["chartInit"] }] } });
4695
4775
 
4696
4776
  function buildBarChartOptions(data, multiSeries, horizontal = false) {
4697
- const showScroll = data.length > 12;
4698
- const zoomEnd = showScroll ? Math.floor((12 / data.length) * 100) : 100;
4777
+ const threshold = horizontal ? 6 : 12;
4778
+ const showScroll = data.length > threshold;
4779
+ const zoomEnd = showScroll ? Math.floor((threshold / data.length) * 100) : 100;
4699
4780
  const isMulti = !!multiSeries && multiSeries.length > 0;
4700
4781
  const xAxisData = data.map(item => item.label);
4701
4782
  const series = isMulti
@@ -4900,7 +4981,7 @@ class BarChart {
4900
4981
  toggleExpand = () => this.isExpanded.update(v => !v);
4901
4982
  options = computed(() => buildBarChartOptions(this.data(), this.multiSeries(), this.horizontal()), ...(ngDevMode ? [{ debugName: "options" }] : []));
4902
4983
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BarChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
4903
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: BarChart, isStandalone: true, selector: "lib-bar-chart", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, multiSeries: { classPropertyName: "multiSeries", publicName: "multiSeries", isSignal: true, isRequired: false, transformFunction: null }, horizontal: { classPropertyName: "horizontal", publicName: "horizontal", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"bar-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"bar-chart-header\">\r\n <h3 class=\"bar-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" class=\"chart-wrapper\" />\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}\r\n", styles: [":host{display:block;width:100%;min-width:0}.bar-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.bar-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.bar-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.bar-chart-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.bar-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick"] }] });
4984
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: BarChart, isStandalone: true, selector: "lib-bar-chart", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: false, transformFunction: null }, multiSeries: { classPropertyName: "multiSeries", publicName: "multiSeries", isSignal: true, isRequired: false, transformFunction: null }, horizontal: { classPropertyName: "horizontal", publicName: "horizontal", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"bar-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"bar-chart-header\">\r\n <h3 class=\"bar-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" class=\"chart-wrapper\" />\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}\r\n", styles: [":host{display:block;width:100%;min-width:0}.bar-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.bar-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.bar-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.bar-chart-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.bar-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick", "chartInit"] }] });
4904
4985
  }
4905
4986
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: BarChart, decorators: [{
4906
4987
  type: Component,
@@ -5122,7 +5203,7 @@ class LineChart {
5122
5203
  toggleExpand = () => this.isExpanded.update(v => !v);
5123
5204
  options = computed(() => buildLineChartOptions(this.data(), this.multiSeries()), ...(ngDevMode ? [{ debugName: "options" }] : []));
5124
5205
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: LineChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
5125
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: LineChart, isStandalone: true, selector: "lib-line-chart", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, multiSeries: { classPropertyName: "multiSeries", publicName: "multiSeries", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"line-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"line-chart-header\">\r\n <h3 class=\"line-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" class=\"chart-wrapper\" />\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}\r\n", styles: [":host{display:block;width:100%;min-width:0}.line-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.line-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.line-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.line-chart-header{display:flex;justify-content:space-between;align-items:center}.line-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick"] }] });
5206
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: LineChart, isStandalone: true, selector: "lib-line-chart", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, multiSeries: { classPropertyName: "multiSeries", publicName: "multiSeries", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"line-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"line-chart-header\">\r\n <h3 class=\"line-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" class=\"chart-wrapper\" />\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}\r\n", styles: [":host{display:block;width:100%;min-width:0}.line-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.line-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.line-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.line-chart-header{display:flex;justify-content:space-between;align-items:center}.line-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick", "chartInit"] }] });
5126
5207
  }
5127
5208
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: LineChart, decorators: [{
5128
5209
  type: Component,
@@ -5136,6 +5217,25 @@ function buildDonutChartOptions(data) {
5136
5217
  const total = getTotal(data);
5137
5218
  const showLegend = data.length > 10;
5138
5219
  const splitLegend = data.length > 14;
5220
+ // Legend tooltip: shows name + value on hover
5221
+ const dataMap = new Map(data.map(d => [d.label, d.value]));
5222
+ const legendTooltip = {
5223
+ show: true,
5224
+ backgroundColor: '#ffffff',
5225
+ borderColor: '#d9dcc7',
5226
+ borderWidth: 1,
5227
+ textStyle: {
5228
+ color: UI_CHART_TOKENS.text,
5229
+ fontSize: 12,
5230
+ fontFamily: UI_CHART_TOKENS.fontFamily,
5231
+ },
5232
+ extraCssText: 'border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,0.10);padding:8px 10px;',
5233
+ formatter: (params) => {
5234
+ const name = typeof params === 'string' ? params : params?.name;
5235
+ const val = dataMap.get(name);
5236
+ return val !== undefined ? `${name}: <b>${val}</b>` : name;
5237
+ },
5238
+ };
5139
5239
  let legendConfig = {};
5140
5240
  if (showLegend) {
5141
5241
  if (splitLegend) {
@@ -5153,6 +5253,7 @@ function buildDonutChartOptions(data) {
5153
5253
  itemWidth: 12,
5154
5254
  itemHeight: 12,
5155
5255
  itemGap: 10,
5256
+ tooltip: legendTooltip,
5156
5257
  textStyle: {
5157
5258
  fontSize: 11,
5158
5259
  fontFamily: UI_CHART_TOKENS.fontFamily,
@@ -5171,6 +5272,7 @@ function buildDonutChartOptions(data) {
5171
5272
  itemWidth: 12,
5172
5273
  itemHeight: 12,
5173
5274
  itemGap: 10,
5275
+ tooltip: legendTooltip,
5174
5276
  textStyle: {
5175
5277
  fontSize: 11,
5176
5278
  fontFamily: UI_CHART_TOKENS.fontFamily,
@@ -5193,6 +5295,7 @@ function buildDonutChartOptions(data) {
5193
5295
  itemWidth: 12,
5194
5296
  itemHeight: 12,
5195
5297
  itemGap: 10,
5298
+ tooltip: legendTooltip,
5196
5299
  textStyle: {
5197
5300
  fontSize: 11,
5198
5301
  fontFamily: UI_CHART_TOKENS.fontFamily,
@@ -5357,15 +5460,29 @@ class DonutChart {
5357
5460
  this.drilledGroup.set(clickedItem.label);
5358
5461
  }
5359
5462
  }
5463
+ chartInstance = null;
5464
+ onChartInit(chart) {
5465
+ this.chartInstance = chart;
5466
+ chart.on('legendselectchanged', (event) => {
5467
+ const selected = event.selected;
5468
+ const currentData = this.displayData();
5469
+ const visibleTotal = currentData
5470
+ .filter(item => selected[item.label] !== false)
5471
+ .reduce((sum, item) => sum + item.value, 0);
5472
+ chart.setOption({
5473
+ title: { text: `${visibleTotal}` }
5474
+ });
5475
+ });
5476
+ }
5360
5477
  resetDrillDown() {
5361
5478
  this.drilledGroup.set(null);
5362
5479
  }
5363
5480
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DonutChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
5364
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: DonutChart, isStandalone: true, selector: "lib-donut-chart", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, drillDown: { classPropertyName: "drillDown", publicName: "drillDown", isSignal: true, isRequired: false, transformFunction: null }, groupByPrefix: { classPropertyName: "groupByPrefix", publicName: "groupByPrefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sectionClick: "sectionClick" }, ngImport: i0, template: "<div class=\"donut-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"donut-chart-header\">\r\n <h3 class=\"donut-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" (chartClick)=\"onChartClick($event)\" class=\"chart-wrapper\" />\r\n \r\n @if (drilledGroup()) {\r\n <button class=\"back-btn\" (click)=\"resetDrillDown()\" type=\"button\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m15 18-6-6 6-6\"/></svg>\r\n <span>Volver</span>\r\n </button>\r\n }\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.donut-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.donut-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.donut-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.donut-chart-header{display:flex;justify-content:space-between;align-items:center}.donut-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}.back-btn{position:absolute;top:24px;right:60px;display:flex;align-items:center;gap:6px;background-color:#d9df88;border:1px solid rgba(97,102,31,.2);padding:6px 12px;border-radius:12px;cursor:pointer;color:#61661f;font-family:Inter,sans-serif;font-size:13px;font-weight:500;box-shadow:0 4px 12px #0000000d;transition:all .2s ease;z-index:100}.back-btn:hover{background-color:#61661f;color:#fff;box-shadow:0 6px 14px #61661f33;transform:translateY(-1px)}.back-btn:active{transform:translateY(0)}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick"] }, { kind: "ngmodule", type: CommonModule }] });
5481
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: DonutChart, isStandalone: true, selector: "lib-donut-chart", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, drillDown: { classPropertyName: "drillDown", publicName: "drillDown", isSignal: true, isRequired: false, transformFunction: null }, groupByPrefix: { classPropertyName: "groupByPrefix", publicName: "groupByPrefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sectionClick: "sectionClick" }, host: { properties: { "attr.title": "null" } }, ngImport: i0, template: "<div class=\"donut-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"donut-chart-header\">\r\n <h3 class=\"donut-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" (chartClick)=\"onChartClick($event)\" (chartInit)=\"onChartInit($event)\" class=\"chart-wrapper\" />\r\n \r\n @if (drilledGroup()) {\r\n <button class=\"back-btn\" (click)=\"resetDrillDown()\" type=\"button\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m15 18-6-6 6-6\"/></svg>\r\n <span>Volver</span>\r\n </button>\r\n }\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.donut-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.donut-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.donut-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.donut-chart-header{display:flex;justify-content:space-between;align-items:center}.donut-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}.back-btn{position:absolute;top:24px;right:60px;display:flex;align-items:center;gap:6px;background-color:#d9df88;border:1px solid rgba(97,102,31,.2);padding:6px 12px;border-radius:12px;cursor:pointer;color:#61661f;font-family:Inter,sans-serif;font-size:13px;font-weight:500;box-shadow:0 4px 12px #0000000d;transition:all .2s ease;z-index:100}.back-btn:hover{background-color:#61661f;color:#fff;box-shadow:0 6px 14px #61661f33;transform:translateY(-1px)}.back-btn:active{transform:translateY(0)}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick", "chartInit"] }, { kind: "ngmodule", type: CommonModule }] });
5365
5482
  }
5366
5483
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DonutChart, decorators: [{
5367
5484
  type: Component,
5368
- args: [{ selector: 'lib-donut-chart', standalone: true, imports: [BaseChart, CommonModule], template: "<div class=\"donut-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"donut-chart-header\">\r\n <h3 class=\"donut-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" (chartClick)=\"onChartClick($event)\" class=\"chart-wrapper\" />\r\n \r\n @if (drilledGroup()) {\r\n <button class=\"back-btn\" (click)=\"resetDrillDown()\" type=\"button\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m15 18-6-6 6-6\"/></svg>\r\n <span>Volver</span>\r\n </button>\r\n }\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.donut-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.donut-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.donut-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.donut-chart-header{display:flex;justify-content:space-between;align-items:center}.donut-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}.back-btn{position:absolute;top:24px;right:60px;display:flex;align-items:center;gap:6px;background-color:#d9df88;border:1px solid rgba(97,102,31,.2);padding:6px 12px;border-radius:12px;cursor:pointer;color:#61661f;font-family:Inter,sans-serif;font-size:13px;font-weight:500;box-shadow:0 4px 12px #0000000d;transition:all .2s ease;z-index:100}.back-btn:hover{background-color:#61661f;color:#fff;box-shadow:0 6px 14px #61661f33;transform:translateY(-1px)}.back-btn:active{transform:translateY(0)}\n"] }]
5485
+ args: [{ selector: 'lib-donut-chart', standalone: true, imports: [BaseChart, CommonModule], host: { '[attr.title]': 'null' }, template: "<div class=\"donut-chart-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"donut-chart-header\">\r\n <h3 class=\"donut-chart-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" (chartClick)=\"onChartClick($event)\" (chartInit)=\"onChartInit($event)\" class=\"chart-wrapper\" />\r\n \r\n @if (drilledGroup()) {\r\n <button class=\"back-btn\" (click)=\"resetDrillDown()\" type=\"button\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m15 18-6-6 6-6\"/></svg>\r\n <span>Volver</span>\r\n </button>\r\n }\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.donut-chart-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.donut-chart-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.donut-chart-container.expanded .chart-wrapper{min-height:unset;height:100%}.donut-chart-header{display:flex;justify-content:space-between;align-items:center}.donut-chart-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}.back-btn{position:absolute;top:24px;right:60px;display:flex;align-items:center;gap:6px;background-color:#d9df88;border:1px solid rgba(97,102,31,.2);padding:6px 12px;border-radius:12px;cursor:pointer;color:#61661f;font-family:Inter,sans-serif;font-size:13px;font-weight:500;box-shadow:0 4px 12px #0000000d;transition:all .2s ease;z-index:100}.back-btn:hover{background-color:#61661f;color:#fff;box-shadow:0 6px 14px #61661f33;transform:translateY(-1px)}.back-btn:active{transform:translateY(0)}\n"] }]
5369
5486
  }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], drillDown: [{ type: i0.Input, args: [{ isSignal: true, alias: "drillDown", required: false }] }], groupByPrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "groupByPrefix", required: false }] }], sectionClick: [{ type: i0.Output, args: ["sectionClick"] }] } });
5370
5487
 
5371
5488
  function buildHeatmapOptions(config) {
@@ -5446,18 +5563,24 @@ function buildHeatmapOptions(config) {
5446
5563
  },
5447
5564
  },
5448
5565
  visualMap: {
5566
+ type: 'piecewise',
5449
5567
  min: minVal,
5450
5568
  max: maxVal,
5451
- calculable: false,
5569
+ splitNumber: 5,
5452
5570
  orient: 'horizontal',
5453
5571
  left: 'center',
5454
5572
  bottom: '0',
5455
- itemWidth: 12,
5456
- itemHeight: 120,
5573
+ itemWidth: 14,
5574
+ itemHeight: 14,
5575
+ itemGap: 8,
5576
+ itemSymbol: 'rect',
5577
+ showLabel: false, // Hide the "10-20" piece labels
5578
+ text: ['Más', 'Menos'], // Labels at the max and min ends
5457
5579
  textStyle: {
5458
5580
  color: UI_CHART_TOKENS.muted,
5459
- fontSize: 11,
5581
+ fontSize: 12,
5460
5582
  fontFamily: UI_CHART_TOKENS.fontFamily,
5583
+ fontWeight: 500
5461
5584
  },
5462
5585
  inRange: {
5463
5586
  color: [
@@ -5504,7 +5627,7 @@ class Heatmap {
5504
5627
  }
5505
5628
  options = computed(() => buildHeatmapOptions(this.config()), ...(ngDevMode ? [{ debugName: "options" }] : []));
5506
5629
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Heatmap, deps: [], target: i0.ɵɵFactoryTarget.Component });
5507
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: Heatmap, isStandalone: true, selector: "lib-heatmap", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"heatmap-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"heatmap-header\">\r\n <h3 class=\"heatmap-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" class=\"chart-wrapper\" />\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.heatmap-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.heatmap-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.heatmap-container.expanded .chart-wrapper{min-height:unset;height:100%}.heatmap-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.heatmap-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick"] }] });
5630
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: Heatmap, isStandalone: true, selector: "lib-heatmap", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"heatmap-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"heatmap-header\">\r\n <h3 class=\"heatmap-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <lib-base-chart [options]=\"options()\" class=\"chart-wrapper\" />\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.heatmap-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.heatmap-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{flex:1;min-height:300px;display:block;width:100%;min-width:0}.heatmap-container.expanded .chart-wrapper{min-height:unset;height:100%}.heatmap-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.heatmap-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: BaseChart, selector: "lib-base-chart", inputs: ["options"], outputs: ["chartClick", "chartInit"] }] });
5508
5631
  }
5509
5632
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Heatmap, decorators: [{
5510
5633
  type: Component,
@@ -5513,21 +5636,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5513
5636
 
5514
5637
  class GeoAPIMaps {
5515
5638
  points = input([], ...(ngDevMode ? [{ debugName: "points" }] : []));
5639
+ cameras = input([], ...(ngDevMode ? [{ debugName: "cameras" }] : []));
5640
+ mapContainer;
5516
5641
  map;
5517
5642
  markers = [];
5643
+ cameraMarkers = [];
5518
5644
  constructor() {
5645
+ // Reactivo a cambios de puntos
5519
5646
  effect(() => {
5520
5647
  const pts = this.points();
5521
5648
  if (this.map && pts.length > 0) {
5522
5649
  this.renderPoints(pts);
5523
5650
  }
5524
5651
  });
5652
+ // Reactivo a cambios de cámaras
5653
+ effect(() => {
5654
+ const cams = this.cameras();
5655
+ if (this.map && cams.length > 0) {
5656
+ this.renderCameras(cams);
5657
+ }
5658
+ });
5525
5659
  }
5526
5660
  ngAfterViewInit() {
5527
5661
  this.initMap();
5528
5662
  }
5529
5663
  initMap() {
5530
- const mapElement = document.getElementById('map');
5664
+ const mapElement = this.mapContainer?.nativeElement;
5531
5665
  if (!mapElement)
5532
5666
  return;
5533
5667
  this.map = new google.maps.Map(mapElement, {
@@ -5549,6 +5683,10 @@ class GeoAPIMaps {
5549
5683
  if (this.points().length > 0) {
5550
5684
  this.renderPoints(this.points());
5551
5685
  }
5686
+ // Render cameras if they already exist when map is ready
5687
+ if (this.cameras().length > 0) {
5688
+ this.renderCameras(this.cameras());
5689
+ }
5552
5690
  }
5553
5691
  goToLocation(lat, lng) {
5554
5692
  if (!this.map)
@@ -5583,33 +5721,42 @@ class GeoAPIMaps {
5583
5721
  map: this.map,
5584
5722
  content: dot,
5585
5723
  });
5724
+ const statusColor = point.immobilized ? '#ef4444' : '#3b82f6';
5586
5725
  const content = `
5587
- <div style="padding: 0 12px 12px 12px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-width: 200px; color: #374151;">
5588
- <div style="font-weight: 700; font-size: 15px; color: #111827; margin-bottom: 8px; line-height: 1.2;">
5589
- ${point.subpoenaNumber}
5726
+ <div style="padding:0 12px 12px 12px;font-family:'Inter',-apple-system,sans-serif;
5727
+ min-width:200px;color:#374151;">
5728
+ <div style="font-weight:700;font-size:15px;color:#111827;">
5729
+ ${point.subpoenaNumber}
5730
+ ${point.immobilized ? '<span style="font-size:11px;background:#ef4444;color:white;border-radius:4px;padding:1px 6px;margin-bottom:8px;vertical-align:middle;">Inmovilizado</span>' : ''}
5731
+ </div>
5732
+
5733
+ <div style="font-size:12px;color:#9ca3af;margin-bottom:10px;display:flex;gap:8px;">
5734
+ <span>📍 ${point.locationLabel}</span>
5735
+ </div>
5736
+
5737
+ <div style="display:flex;flex-direction:column;gap:4px;">
5738
+ <div style="display:flex;justify-content:space-between;gap:16px;font-size:13px;">
5739
+ <span style="color:#9ca3af;">Fecha</span>
5740
+ <span style="font-weight:600;color:#374151;">${new Date(point.dateTime).toLocaleString('es-CO', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })}</span>
5590
5741
  </div>
5591
- <div style="font-size: 14px; color: #4b5563; margin-bottom: 4px;">
5592
- ${point.locationLabel}
5742
+ <div style="display:flex;justify-content:space-between;gap:16px;font-size:13px;">
5743
+ <span style="color:#9ca3af;">Infracción</span>
5744
+ <span style="font-weight:600;color:#374151;">${point.infractionCode}</span>
5593
5745
  </div>
5594
- <div style="font-size: 14px; color: #6b7280; margin-bottom: 4px;">
5595
- ${point.dateTime.replace('T', ' - ').substring(0, 18)}
5746
+ <div style="display:flex;justify-content:space-between;gap:16px;font-size:13px;">
5747
+ <span style="color:#9ca3af;">Placa</span>
5748
+ <span style="font-weight:600;color:#374151;">${point.plate}</span>
5596
5749
  </div>
5597
- <div style="display: flex; flex-direction: column; gap: 4px; margin-top: 4px;">
5598
- <div style="font-size: 13px;">
5599
- <span style="color: #9ca3af;">Infracción:</span>
5600
- <span style="font-weight: 500; color: #374151;">${point.infractionCode}</span>
5601
- </div>
5602
- <div style="font-size: 13px;">
5603
- <span style="color: #9ca3af;">Placa:</span>
5604
- <span style="font-weight: 500; color: #374151;">${point.plate}</span>
5605
- </div>
5606
- <div style="display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; margin-top: 4px; color: ${point.immobilized ? '#ef4444' : '#3b82f6'};">
5607
- <span style="width: 8px; height: 8px; border-radius: 50%; background-color: ${point.immobilized ? '#ef4444' : '#3b82f6'};"></span>
5750
+ <div style="border-top:1px solid #e5e7eb;margin-top:6px;padding-top:6px;
5751
+ display:flex;justify-content:space-between;font-size:13px;font-weight:700;">
5752
+ <span style="color:${statusColor};">Estado</span>
5753
+ <span style="display:flex;align-items:center;gap:6px;color:${statusColor};">
5754
+ <span style="width:8px;height:8px;border-radius:50%;background-color:${statusColor};"></span>
5608
5755
  ${point.immobilized ? 'Inmovilizado' : 'Normal'}
5609
- </div>
5756
+ </span>
5610
5757
  </div>
5611
5758
  </div>
5612
- `;
5759
+ </div>`;
5613
5760
  marker.addListener('mouseover', () => {
5614
5761
  if (!lockedMarker) {
5615
5762
  infoWindow.setContent(content);
@@ -5638,28 +5785,124 @@ class GeoAPIMaps {
5638
5785
  this.map.fitBounds(bounds);
5639
5786
  }
5640
5787
  }
5788
+ // ─── GeoPointCameras markers ─────────────────────────────────────────────────
5789
+ clearCameraMarkers() {
5790
+ this.cameraMarkers.forEach(m => (m.map = null));
5791
+ this.cameraMarkers = [];
5792
+ }
5793
+ renderCameras(cameras) {
5794
+ this.clearCameraMarkers();
5795
+ const infoWindow = new google.maps.InfoWindow();
5796
+ let lockedMarker = null;
5797
+ // 1. Encontrar el total mayor para destacarlo
5798
+ const maxTotal = Math.max(...cameras.map(c => c.total));
5799
+ // 2. Centrar el mapa para que se vean todas las cámaras al inicio
5800
+ if (cameras.length > 0) {
5801
+ const bounds = new google.maps.LatLngBounds();
5802
+ cameras.forEach(c => bounds.extend({ lat: c.latitude, lng: c.longitude }));
5803
+ this.map.fitBounds(bounds);
5804
+ }
5805
+ cameras.forEach(cam => {
5806
+ // 3. Color diferente si es el de mayor total
5807
+ const isTop = cam.total === maxTotal;
5808
+ const bgColor = isTop ? '#dc2626' : '#7c3aed'; // rojo para el top, morado para el resto
5809
+ const pin = document.createElement('div');
5810
+ pin.innerHTML = `
5811
+ <div style="
5812
+ background:${bgColor}; color:white; border-radius:8px;
5813
+ padding:4px 8px; font-size:12px; font-weight:700;
5814
+ border:2px solid white; box-shadow:0 2px 6px rgba(0,0,0,.3);
5815
+ display:flex; align-items:center; gap:4px; cursor:pointer; white-space:nowrap;">
5816
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24"
5817
+ fill="white">
5818
+ <path d="M15 8v8H5V8h10m1-2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0
5819
+ 1-1v-3.5l4 4v-11l-4 4V7a1 1 0 0 0-1-1z"/>
5820
+ </svg>
5821
+ ${cam.total}
5822
+ </div>`;
5823
+ const marker = new google.maps.marker.AdvancedMarkerElement({
5824
+ position: { lat: cam.latitude, lng: cam.longitude },
5825
+ map: this.map,
5826
+ content: pin,
5827
+ title: cam.name,
5828
+ });
5829
+ const codesHtml = Object.entries(cam.by_code)
5830
+ .map(([code, count]) => `
5831
+ <div style="display:flex;justify-content:space-between;gap:16px;font-size:13px;">
5832
+ <span style="color:#9ca3af;">${code}</span>
5833
+ <span style="font-weight:600;color:#374151;">${count}</span>
5834
+ </div>`)
5835
+ .join('');
5836
+ const content = `
5837
+ <div style="padding:0 12px 12px 12px;font-family:'Inter',-apple-system,sans-serif;
5838
+ min-width:200px;color:#374151;">
5839
+ <div style="font-weight:700;font-size:15px;color:#111827;">
5840
+ ${cam.name}
5841
+ ${isTop ? '<span style="font-size:11px;background:#dc2626;color:white;border-radius:4px;padding:1px 6px;margin-bottom:8px;vertical-align:middle;">Más Infracciones</span>' : ''}
5842
+ </div>
5843
+ <!-- <div style="font-size:12px;color:#9ca3af;margin-bottom:4px;">ID: ${cam.id}</div> -->
5844
+
5845
+ <!-- 4. Coordenadas en el tooltip -->
5846
+ <div style="font-size:12px;color:#9ca3af;margin-bottom:10px;display:flex;gap:8px;">
5847
+ <span>📍 ${cam.locationLabel}</span>
5848
+ </div>
5849
+
5850
+ <div style="display:flex;flex-direction:column;gap:4px;">
5851
+ <div style="margin-top:8px;font-size:14px;color:#6b7280;">Código(s) de Infracciones</div>
5852
+ ${codesHtml}
5853
+ <div style="border-top:1px solid #e5e7eb;margin-top:6px;padding-top:6px;
5854
+ display:flex;justify-content:space-between;font-size:13px;font-weight:700;">
5855
+ <span style="color:${bgColor};">Total</span>
5856
+ <span style="color:${bgColor};">${cam.total}</span>
5857
+ </div>
5858
+ </div>
5859
+ </div>`;
5860
+ marker.addListener('mouseover', () => {
5861
+ if (!lockedMarker) {
5862
+ infoWindow.setContent(content);
5863
+ infoWindow.open(this.map, marker);
5864
+ }
5865
+ });
5866
+ marker.addListener('mouseout', () => { if (!lockedMarker)
5867
+ infoWindow.close(); });
5868
+ marker.addListener('click', () => {
5869
+ lockedMarker = marker;
5870
+ infoWindow.setContent(content);
5871
+ infoWindow.open(this.map, marker);
5872
+ });
5873
+ infoWindow.addListener('closeclick', () => { lockedMarker = null; });
5874
+ this.cameraMarkers.push(marker);
5875
+ });
5876
+ }
5641
5877
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: GeoAPIMaps, deps: [], target: i0.ɵɵFactoryTarget.Component });
5642
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: GeoAPIMaps, isStandalone: true, selector: "lib-geo-api-maps", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div id=\"map\"></div>\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative}#map{position:absolute;inset:0;z-index:1;border-radius:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
5878
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.16", type: GeoAPIMaps, isStandalone: true, selector: "lib-geo-api-maps", inputs: { points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: false, transformFunction: null }, cameras: { classPropertyName: "cameras", publicName: "cameras", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "mapContainer", first: true, predicate: ["mapContainer"], descendants: true, static: true }], ngImport: i0, template: "<div #mapContainer class=\"map\"></div>\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative}.map{position:absolute;inset:0;z-index:1;border-radius:12px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }] });
5643
5879
  }
5644
5880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: GeoAPIMaps, decorators: [{
5645
5881
  type: Component,
5646
- args: [{ selector: 'lib-geo-api-maps', standalone: true, imports: [CommonModule], template: "<div id=\"map\"></div>\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative}#map{position:absolute;inset:0;z-index:1;border-radius:12px}\n"] }]
5647
- }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: false }] }] } });
5882
+ args: [{ selector: 'lib-geo-api-maps', standalone: true, imports: [CommonModule], template: "<div #mapContainer class=\"map\"></div>\r\n", styles: [":host{display:block;width:100%;height:100%;position:relative}.map{position:absolute;inset:0;z-index:1;border-radius:12px}\n"] }]
5883
+ }], ctorParameters: () => [], propDecorators: { points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: false }] }], cameras: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameras", required: false }] }], mapContainer: [{
5884
+ type: ViewChild,
5885
+ args: ['mapContainer', { static: true }]
5886
+ }] } });
5648
5887
 
5649
5888
  class MapGeo {
5650
5889
  title = input('Mapa Geográfico', ...(ngDevMode ? [{ debugName: "title" }] : []));
5651
5890
  isExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isExpanded" }] : []));
5652
5891
  points = input([], ...(ngDevMode ? [{ debugName: "points" }] : []));
5892
+ cameras = input([], ...(ngDevMode ? [{ debugName: "cameras" }] : []));
5893
+ immobilizedCount = computed(() => this.points().filter(p => p.immobilized).length, ...(ngDevMode ? [{ debugName: "immobilizedCount" }] : []));
5894
+ normalCount = computed(() => this.points().filter(p => !p.immobilized).length, ...(ngDevMode ? [{ debugName: "normalCount" }] : []));
5895
+ totalCameras = computed(() => this.cameras().length, ...(ngDevMode ? [{ debugName: "totalCameras" }] : []));
5653
5896
  toggleExpand() {
5654
5897
  this.isExpanded.update((value) => !value);
5655
5898
  }
5656
5899
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MapGeo, deps: [], target: i0.ɵɵFactoryTarget.Component });
5657
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MapGeo, isStandalone: true, selector: "lib-map-geo", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"map-geo-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"map-geo-header\">\r\n <h3 class=\"map-geo-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <div class=\"chart-wrapper\">\r\n <lib-geo-api-maps [points]=\"points()\" />\r\n </div>\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.map-geo-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.map-geo-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{position:relative;width:100%;height:450px;min-height:450px;display:block;min-width:0}.map-geo-container.expanded .chart-wrapper{flex:1;height:auto;min-height:0}.map-geo-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.map-geo-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"], dependencies: [{ kind: "component", type: GeoAPIMaps, selector: "lib-geo-api-maps", inputs: ["points"] }] });
5900
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.16", type: MapGeo, isStandalone: true, selector: "lib-map-geo", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, points: { classPropertyName: "points", publicName: "points", isSignal: true, isRequired: false, transformFunction: null }, cameras: { classPropertyName: "cameras", publicName: "cameras", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<div class=\"map-geo-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"map-geo-header\">\r\n <h3 class=\"map-geo-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <div class=\"chart-wrapper\">\r\n <lib-geo-api-maps [points]=\"points()\" [cameras]=\"cameras()\" />\r\n </div>\r\n @if (points().length > 0) {\r\n <div class=\"map-summary\">\r\n <span class=\"map-badge blue\">\r\n <span class=\"map-dot blue\"></span>\r\n Normal: {{ normalCount() }}\r\n </span>\r\n <span class=\"map-badge red\">\r\n <span class=\"map-dot red\"></span>\r\n Inmovilizado: {{ immobilizedCount() }}\r\n </span>\r\n <span class=\"map-badge total\">Total: {{ points().length }}</span>\r\n </div>\r\n }\r\n @if (cameras().length > 0) {\r\n <div class=\"map-summary\">\r\n <span class=\"map-badge purple\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M15 8v8H5V8h10m1-2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11l-4 4V7a1 1 0 0 0-1-1z\"/>\r\n </svg>\r\n C\u00E1maras: {{ totalCameras() }}\r\n </span>\r\n </div>\r\n }\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.map-geo-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.map-geo-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{position:relative;width:100%;height:450px;min-height:450px;display:block;min-width:0}.map-geo-container.expanded .chart-wrapper{flex:1;height:auto;min-height:0}.map-geo-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.map-geo-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}.map-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.map-badge{display:inline-flex;align-items:center;gap:5px;font-family:Inter,sans-serif;font-size:12px;font-weight:600;padding:4px 10px;border-radius:20px;white-space:nowrap}.map-badge.blue{background-color:#3b82f61f;color:#2563eb}.map-badge.red{background-color:#ef44441f;color:#dc2626}.map-badge.purple{background-color:#7c3aed1f;color:#7c3aed}.map-badge.total{background-color:#3741511a;color:#374151}.map-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.map-dot.blue{background-color:#3b82f6}.map-dot.red{background-color:#ef4444}\n"], dependencies: [{ kind: "component", type: GeoAPIMaps, selector: "lib-geo-api-maps", inputs: ["points", "cameras"] }] });
5658
5901
  }
5659
5902
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MapGeo, decorators: [{
5660
5903
  type: Component,
5661
- args: [{ selector: 'lib-map-geo', imports: [GeoAPIMaps], template: "<div class=\"map-geo-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"map-geo-header\">\r\n <h3 class=\"map-geo-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <div class=\"chart-wrapper\">\r\n <lib-geo-api-maps [points]=\"points()\" />\r\n </div>\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.map-geo-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.map-geo-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{position:relative;width:100%;height:450px;min-height:450px;display:block;min-width:0}.map-geo-container.expanded .chart-wrapper{flex:1;height:auto;min-height:0}.map-geo-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.map-geo-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}\n"] }]
5662
- }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: false }] }] } });
5904
+ args: [{ selector: 'lib-map-geo', imports: [GeoAPIMaps], template: "<div class=\"map-geo-container\" [class.expanded]=\"isExpanded()\">\r\n <div class=\"map-geo-header\">\r\n <h3 class=\"map-geo-title\">{{ title() }}</h3>\r\n <button class=\"expand-btn\" (click)=\"toggleExpand()\" type=\"button\" [attr.aria-label]=\"isExpanded() ? 'Minimize chart' : 'Maximize chart'\">\r\n @if (isExpanded()) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-minimize-2\"><polyline points=\"4 14 10 14 10 20\"/><polyline points=\"20 10 14 10 14 4\"/><line x1=\"14\" x2=\"21\" y1=\"10\" y2=\"3\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-maximize-2\"><polyline points=\"15 3 21 3 21 9\"/><polyline points=\"9 21 3 21 3 15\"/><line x1=\"21\" x2=\"14\" y1=\"3\" y2=\"10\"/><line x1=\"3\" x2=\"10\" y1=\"21\" y2=\"14\"/></svg>\r\n }\r\n </button>\r\n </div>\r\n <div class=\"chart-wrapper\">\r\n <lib-geo-api-maps [points]=\"points()\" [cameras]=\"cameras()\" />\r\n </div>\r\n @if (points().length > 0) {\r\n <div class=\"map-summary\">\r\n <span class=\"map-badge blue\">\r\n <span class=\"map-dot blue\"></span>\r\n Normal: {{ normalCount() }}\r\n </span>\r\n <span class=\"map-badge red\">\r\n <span class=\"map-dot red\"></span>\r\n Inmovilizado: {{ immobilizedCount() }}\r\n </span>\r\n <span class=\"map-badge total\">Total: {{ points().length }}</span>\r\n </div>\r\n }\r\n @if (cameras().length > 0) {\r\n <div class=\"map-summary\">\r\n <span class=\"map-badge purple\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\r\n <path d=\"M15 8v8H5V8h10m1-2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3.5l4 4v-11l-4 4V7a1 1 0 0 0-1-1z\"/>\r\n </svg>\r\n C\u00E1maras: {{ totalCameras() }}\r\n </span>\r\n </div>\r\n }\r\n</div>\r\n@if (isExpanded()) {\r\n <div class=\"overlay\" (click)=\"toggleExpand()\"></div>\r\n}", styles: [":host{display:block;width:100%;min-width:0}.map-geo-container{width:100%;min-width:0;display:flex;flex-direction:column;gap:16px;background-color:#f0f0db;padding:24px 16px 16px;border-radius:20px;position:relative;transition:all .3s ease}.map-geo-container.expanded{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:90vw;height:90vh;z-index:9999;box-shadow:0 25px 50px -12px #00000040;margin:0}.chart-wrapper{position:relative;width:100%;height:450px;min-height:450px;display:block;min-width:0}.map-geo-container.expanded .chart-wrapper{flex:1;height:auto;min-height:0}.map-geo-header{display:flex;justify-content:space-between;align-items:center;gap:16px}.map-geo-title{font-size:20px;font-weight:600;color:#333;font-family:Inter,sans-serif}.expand-btn{background:transparent;border:none;cursor:pointer;color:#61661f;padding:6px;border-radius:8px;display:flex;align-items:center;justify-content:center;transition:background-color .2s}.expand-btn:hover{background-color:#61661f1a}.overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:#0006;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);z-index:9998}.map-summary{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.map-badge{display:inline-flex;align-items:center;gap:5px;font-family:Inter,sans-serif;font-size:12px;font-weight:600;padding:4px 10px;border-radius:20px;white-space:nowrap}.map-badge.blue{background-color:#3b82f61f;color:#2563eb}.map-badge.red{background-color:#ef44441f;color:#dc2626}.map-badge.purple{background-color:#7c3aed1f;color:#7c3aed}.map-badge.total{background-color:#3741511a;color:#374151}.map-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.map-dot.blue{background-color:#3b82f6}.map-dot.red{background-color:#ef4444}\n"] }]
5905
+ }], propDecorators: { title: [{ type: i0.Input, args: [{ isSignal: true, alias: "title", required: false }] }], points: [{ type: i0.Input, args: [{ isSignal: true, alias: "points", required: false }] }], cameras: [{ type: i0.Input, args: [{ isSignal: true, alias: "cameras", required: false }] }] } });
5663
5906
 
5664
5907
  class TableChart {
5665
5908
  title = input('Table Chart', ...(ngDevMode ? [{ debugName: "title" }] : []));