sapenlinea-components 0.9.75 → 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,
@@ -5135,6 +5216,104 @@ function getTotal(data) {
5135
5216
  function buildDonutChartOptions(data) {
5136
5217
  const total = getTotal(data);
5137
5218
  const showLegend = data.length > 10;
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
+ };
5239
+ let legendConfig = {};
5240
+ if (showLegend) {
5241
+ if (splitLegend) {
5242
+ const half = Math.ceil(data.length / 2);
5243
+ const leftData = data.slice(0, half).map(d => d.label);
5244
+ const rightData = data.slice(half).map(d => d.label);
5245
+ legendConfig = {
5246
+ legend: [
5247
+ {
5248
+ data: leftData,
5249
+ type: 'scroll',
5250
+ orient: 'vertical',
5251
+ left: '5%',
5252
+ top: 'center',
5253
+ itemWidth: 12,
5254
+ itemHeight: 12,
5255
+ itemGap: 10,
5256
+ tooltip: legendTooltip,
5257
+ textStyle: {
5258
+ fontSize: 11,
5259
+ fontFamily: UI_CHART_TOKENS.fontFamily,
5260
+ color: UI_CHART_TOKENS.muted,
5261
+ },
5262
+ pageTextStyle: { color: UI_CHART_TOKENS.muted },
5263
+ pageIconColor: UI_CHART_TOKENS.centerText,
5264
+ pageIconInactiveColor: '#ccc',
5265
+ },
5266
+ {
5267
+ data: rightData,
5268
+ type: 'scroll',
5269
+ orient: 'vertical',
5270
+ right: '5%',
5271
+ top: 'center',
5272
+ itemWidth: 12,
5273
+ itemHeight: 12,
5274
+ itemGap: 10,
5275
+ tooltip: legendTooltip,
5276
+ textStyle: {
5277
+ fontSize: 11,
5278
+ fontFamily: UI_CHART_TOKENS.fontFamily,
5279
+ color: UI_CHART_TOKENS.muted,
5280
+ },
5281
+ pageTextStyle: { color: UI_CHART_TOKENS.muted },
5282
+ pageIconColor: UI_CHART_TOKENS.centerText,
5283
+ pageIconInactiveColor: '#ccc',
5284
+ }
5285
+ ]
5286
+ };
5287
+ }
5288
+ else {
5289
+ legendConfig = {
5290
+ legend: {
5291
+ type: 'scroll',
5292
+ orient: 'vertical',
5293
+ right: '5%',
5294
+ top: 'center',
5295
+ itemWidth: 12,
5296
+ itemHeight: 12,
5297
+ itemGap: 10,
5298
+ tooltip: legendTooltip,
5299
+ textStyle: {
5300
+ fontSize: 11,
5301
+ fontFamily: UI_CHART_TOKENS.fontFamily,
5302
+ color: UI_CHART_TOKENS.muted,
5303
+ },
5304
+ pageTextStyle: {
5305
+ color: UI_CHART_TOKENS.muted,
5306
+ },
5307
+ pageIconColor: UI_CHART_TOKENS.centerText,
5308
+ pageIconInactiveColor: '#ccc',
5309
+ }
5310
+ };
5311
+ }
5312
+ }
5313
+ let centerX = '50%';
5314
+ if (showLegend) {
5315
+ centerX = splitLegend ? '50%' : '35%';
5316
+ }
5138
5317
  return {
5139
5318
  animation: true,
5140
5319
  backgroundColor: 'transparent',
@@ -5164,32 +5343,12 @@ function buildDonutChartOptions(data) {
5164
5343
  `,
5165
5344
  formatter: (params) => `${params.name}: ${params.value}`,
5166
5345
  },
5167
- ...(showLegend ? {
5168
- legend: {
5169
- type: 'scroll',
5170
- orient: 'horizontal',
5171
- bottom: 0,
5172
- left: 'center',
5173
- itemWidth: 12,
5174
- itemHeight: 12,
5175
- itemGap: 10,
5176
- textStyle: {
5177
- fontSize: 11,
5178
- fontFamily: UI_CHART_TOKENS.fontFamily,
5179
- color: UI_CHART_TOKENS.muted,
5180
- },
5181
- pageTextStyle: {
5182
- color: UI_CHART_TOKENS.muted,
5183
- },
5184
- pageIconColor: UI_CHART_TOKENS.centerText,
5185
- pageIconInactiveColor: '#ccc',
5186
- },
5187
- } : {}),
5346
+ ...legendConfig,
5188
5347
  series: [
5189
5348
  {
5190
5349
  type: 'pie',
5191
5350
  radius: ['44%', '72%'],
5192
- center: ['50%', showLegend ? '46%' : '52%'],
5351
+ center: [centerX, '50%'],
5193
5352
  avoidLabelOverlap: true,
5194
5353
  minAngle: 4,
5195
5354
  itemStyle: {
@@ -5242,21 +5401,19 @@ function buildDonutChartOptions(data) {
5242
5401
  })),
5243
5402
  },
5244
5403
  ],
5245
- graphic: [
5246
- {
5247
- type: 'text',
5248
- left: 'center',
5249
- top: showLegend ? '42%' : '48%',
5250
- style: {
5251
- text: `${total}`,
5252
- textAlign: 'center',
5253
- fill: UI_CHART_TOKENS.centerText,
5254
- fontSize: 24,
5255
- fontWeight: 700,
5256
- fontFamily: UI_CHART_TOKENS.fontFamily,
5257
- },
5404
+ title: {
5405
+ text: `${total}`,
5406
+ left: centerX,
5407
+ top: '48%',
5408
+ textAlign: 'center',
5409
+ textVerticalAlign: 'middle',
5410
+ textStyle: {
5411
+ color: UI_CHART_TOKENS.centerText,
5412
+ fontSize: 24,
5413
+ fontWeight: 700,
5414
+ fontFamily: UI_CHART_TOKENS.fontFamily,
5258
5415
  },
5259
- ],
5416
+ },
5260
5417
  };
5261
5418
  }
5262
5419
  class DonutChart {
@@ -5303,15 +5460,29 @@ class DonutChart {
5303
5460
  this.drilledGroup.set(clickedItem.label);
5304
5461
  }
5305
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
+ }
5306
5477
  resetDrillDown() {
5307
5478
  this.drilledGroup.set(null);
5308
5479
  }
5309
5480
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DonutChart, deps: [], target: i0.ɵɵFactoryTarget.Component });
5310
- 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 }] });
5311
5482
  }
5312
5483
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: DonutChart, decorators: [{
5313
5484
  type: Component,
5314
- 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"] }]
5315
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"] }] } });
5316
5487
 
5317
5488
  function buildHeatmapOptions(config) {
@@ -5392,17 +5563,24 @@ function buildHeatmapOptions(config) {
5392
5563
  },
5393
5564
  },
5394
5565
  visualMap: {
5566
+ type: 'piecewise',
5395
5567
  min: minVal,
5396
5568
  max: maxVal,
5569
+ splitNumber: 5,
5397
5570
  orient: 'horizontal',
5398
5571
  left: 'center',
5399
5572
  bottom: '0',
5400
- itemWidth: 12,
5401
- 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
5402
5579
  textStyle: {
5403
5580
  color: UI_CHART_TOKENS.muted,
5404
- fontSize: 11,
5581
+ fontSize: 12,
5405
5582
  fontFamily: UI_CHART_TOKENS.fontFamily,
5583
+ fontWeight: 500
5406
5584
  },
5407
5585
  inRange: {
5408
5586
  color: [
@@ -5449,7 +5627,7 @@ class Heatmap {
5449
5627
  }
5450
5628
  options = computed(() => buildHeatmapOptions(this.config()), ...(ngDevMode ? [{ debugName: "options" }] : []));
5451
5629
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Heatmap, deps: [], target: i0.ɵɵFactoryTarget.Component });
5452
- 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"] }] });
5453
5631
  }
5454
5632
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: Heatmap, decorators: [{
5455
5633
  type: Component,
@@ -5458,21 +5636,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImpo
5458
5636
 
5459
5637
  class GeoAPIMaps {
5460
5638
  points = input([], ...(ngDevMode ? [{ debugName: "points" }] : []));
5639
+ cameras = input([], ...(ngDevMode ? [{ debugName: "cameras" }] : []));
5640
+ mapContainer;
5461
5641
  map;
5462
5642
  markers = [];
5643
+ cameraMarkers = [];
5463
5644
  constructor() {
5645
+ // Reactivo a cambios de puntos
5464
5646
  effect(() => {
5465
5647
  const pts = this.points();
5466
5648
  if (this.map && pts.length > 0) {
5467
5649
  this.renderPoints(pts);
5468
5650
  }
5469
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
+ });
5470
5659
  }
5471
5660
  ngAfterViewInit() {
5472
5661
  this.initMap();
5473
5662
  }
5474
5663
  initMap() {
5475
- const mapElement = document.getElementById('map');
5664
+ const mapElement = this.mapContainer?.nativeElement;
5476
5665
  if (!mapElement)
5477
5666
  return;
5478
5667
  this.map = new google.maps.Map(mapElement, {
@@ -5494,6 +5683,10 @@ class GeoAPIMaps {
5494
5683
  if (this.points().length > 0) {
5495
5684
  this.renderPoints(this.points());
5496
5685
  }
5686
+ // Render cameras if they already exist when map is ready
5687
+ if (this.cameras().length > 0) {
5688
+ this.renderCameras(this.cameras());
5689
+ }
5497
5690
  }
5498
5691
  goToLocation(lat, lng) {
5499
5692
  if (!this.map)
@@ -5528,33 +5721,42 @@ class GeoAPIMaps {
5528
5721
  map: this.map,
5529
5722
  content: dot,
5530
5723
  });
5724
+ const statusColor = point.immobilized ? '#ef4444' : '#3b82f6';
5531
5725
  const content = `
5532
- <div style="padding: 0 12px 12px 12px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-width: 200px; color: #374151;">
5533
- <div style="font-weight: 700; font-size: 15px; color: #111827; margin-bottom: 8px; line-height: 1.2;">
5534
- ${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>
5535
5741
  </div>
5536
- <div style="font-size: 14px; color: #4b5563; margin-bottom: 4px;">
5537
- ${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>
5538
5745
  </div>
5539
- <div style="font-size: 14px; color: #6b7280; margin-bottom: 4px;">
5540
- ${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>
5541
5749
  </div>
5542
- <div style="display: flex; flex-direction: column; gap: 4px; margin-top: 4px;">
5543
- <div style="font-size: 13px;">
5544
- <span style="color: #9ca3af;">Infracción:</span>
5545
- <span style="font-weight: 500; color: #374151;">${point.infractionCode}</span>
5546
- </div>
5547
- <div style="font-size: 13px;">
5548
- <span style="color: #9ca3af;">Placa:</span>
5549
- <span style="font-weight: 500; color: #374151;">${point.plate}</span>
5550
- </div>
5551
- <div style="display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; margin-top: 4px; color: ${point.immobilized ? '#ef4444' : '#3b82f6'};">
5552
- <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>
5553
5755
  ${point.immobilized ? 'Inmovilizado' : 'Normal'}
5554
- </div>
5756
+ </span>
5555
5757
  </div>
5556
5758
  </div>
5557
- `;
5759
+ </div>`;
5558
5760
  marker.addListener('mouseover', () => {
5559
5761
  if (!lockedMarker) {
5560
5762
  infoWindow.setContent(content);
@@ -5583,28 +5785,124 @@ class GeoAPIMaps {
5583
5785
  this.map.fitBounds(bounds);
5584
5786
  }
5585
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
+ }
5586
5877
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: GeoAPIMaps, deps: [], target: i0.ɵɵFactoryTarget.Component });
5587
- 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 }] });
5588
5879
  }
5589
5880
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: GeoAPIMaps, decorators: [{
5590
5881
  type: Component,
5591
- 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"] }]
5592
- }], 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
+ }] } });
5593
5887
 
5594
5888
  class MapGeo {
5595
5889
  title = input('Mapa Geográfico', ...(ngDevMode ? [{ debugName: "title" }] : []));
5596
5890
  isExpanded = signal(false, ...(ngDevMode ? [{ debugName: "isExpanded" }] : []));
5597
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" }] : []));
5598
5896
  toggleExpand() {
5599
5897
  this.isExpanded.update((value) => !value);
5600
5898
  }
5601
5899
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MapGeo, deps: [], target: i0.ɵɵFactoryTarget.Component });
5602
- 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"] }] });
5603
5901
  }
5604
5902
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.16", ngImport: i0, type: MapGeo, decorators: [{
5605
5903
  type: Component,
5606
- 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"] }]
5607
- }], 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 }] }] } });
5608
5906
 
5609
5907
  class TableChart {
5610
5908
  title = input('Table Chart', ...(ngDevMode ? [{ debugName: "title" }] : []));