Rafael Lopez

template obejtos agregado

1 +
2 +A fines de 2025 decidimos abrir los datos históricos de presupuesto del Estado boliviano.
3 +
4 +Cada año el Estado recibe y gasta dinero a tu nombre. ¿Sabes de dónde viene, en qué se usa y qué se logra con él? Esa información es tuya. Siempre lo fue, pero durante décadas permaneció encerrada en archivos PDF, en sistemas que pocos saben usar y en las paredes de un ministerio.
5 +
6 +Hoy cambia eso.
7 +
8 +Aquí puedes explorar más de veinte años de ejecución presupuestaria, que serán actualizados semanalmente. Puedes ver qué hizo el Estado en tu región, en tu sector y en el año que te interesa. Puedes comparar, cuestionar y entender. Y si encuentras algo que no cuadra o que vale la pena contar, puedes hacerlo tuyo.
9 +
10 +No publicamos ésto para cumplir un trámite de transparencia. Lo hacemos porque creemos que un Estado que trabaja de espaldas a su sociedad se vuelve impune, y una sociedad que no entiende al Estado se desconecta de él.
11 +
12 +Un presupuesto abierto es una apuesta: que la confianza se construye con información y no con promesas.
13 +
14 +Abrimos para no cerrar.
15 +
16 +# La forma
17 +
18 +Existen 2 bases del presupuesto: gastos e ingresos, que registran el destino y origen de los recursos públicos respectivamente. Ambas bases cubren información sobre todas las entidades del Estado entre 2005 y 2025.
19 +
20 +Cada registro de gasto o ingreso está compuesto de atributos que permiten identificarlo y de montos en bolivianos nominales. Los atributos que identifican a cada gasto son:
21 +
22 +- Clasificador institucional: la entidad pública y dirección administrativa que registra el gasto.
23 +- Categoría programática: el programa, proyecto o actividad al que se dirige.
24 +- Objeto de gasto: el tipo de gasto según un clasificador estandarizado.
25 +- Financiamiento: las fuentes de los recursos utilzados.
26 +
27 +Mientras que aquellos que identifican a un ingreso son:
28 +
29 +- Clasificador institucional: la entidad pública que registra el ingreso.
30 +- Rubro de ingreso: el tipo de ingreso según un clasificador estandarizado.
31 +- Financiamiento: las fuentes del ingreso.
32 +
33 +Cada registro incluye 4 montos:
34 +
35 +- Inicial: lo registrado en el presupuesto a inicio de la gestión.
36 +- Vigente: el presupuesto luego de modificaciones durante la gestión.
37 +- Devengado: el monto devengado para ejecución.
38 +- Ejecutado: el monto pagado o percibido.
39 +
40 +Además, cuando están disponibles, contamos con atributos adicionales para describir mejor el destino de los gastos. Éstos son finalidad y función, sector económico y ubicación geográfica.
41 +
42 +# El detalle
43 +
44 +## Gastos
45 +
46 +La base de gastos nos permite comprender el destino de los recursos públicos.
47 +
48 +- **Quién**: entidad y dirección administrativa responsable.
49 +- **Qué**: programas, proyectos y actividades a los que se dirigen recursos.
50 +- **Para qué**: finalidad y función pública.
51 +- **En qué**: objetos de gasto.
52 +- **Con qué recursos**: fuentes de las que se toman recursos.
53 +- **Cuánto**: montos de presupuesto y ejecución.
54 +
55 +Los campos en esta base son los siguientes.
56 +
57 +### Gestión
58 +
59 +#### gestion
60 +
61 +Año fiscal del registro presupuestario.
62 +
63 +Tipo: Número entero de 4 dígitos.
64 +
65 +Ejemplo: `2005`.
66 +
67 +### Clasificador institucional
68 +
69 +Información sobre qué entidad y dirección administrativa registra el gasto. Una dirección administrativa pertenece a una entidad que a su vez está posicionada dentro de una jerarquía en el aparato estatal:
70 +
71 +```
72 +Sector
73 +└─ Subsector
74 + └─ Área
75 + └─ Subárea
76 + └─ Entidad
77 + └─ Dirección Administrativa
78 +```
79 +
80 +#### entidad
81 +
82 +Código de la entidad pública a la que pertenece el registro. Cada entidad ocupa una posición en el Sector Público que puede ser descrita en una jerarquía de 4 niveles: sector, subsector, área y subárea. Para cada nivel contamos con un código, sigla y descripción. Dentro de cada entidad, el gasto es registrado en una dirección administrativa, a la que podemos describir con un código y descripción.
83 +
84 +Tipo: Número entero de 1 a 4 dígitos.
85 +
86 +Ejemplo: `10`.
87 +
88 +#### entidad_desc_entidad
89 +
90 +Descripción de la entidad pública.
91 +
92 +Tipo: Texto descriptivo.
93 +
94 +Ejemplo: `Ministerio De Relaciones Exteriores Y Culto`.
95 +
96 +#### entidad_sigla_entidad
97 +
98 +Sigla de la entidad pública.
99 +
100 +Tipo: Texto corto (sigla).
101 +
102 +Ejemplo: `MIN-RREE`.
103 +
104 +#### entidad_sector
105 +
106 +Código del sector al que pertenece la entidad pública.
107 +
108 +Tipo: Número entero de 1 dígito.
109 +
110 +Ejemplo: `1`.
111 +
112 +#### entidad_subsector
113 +
114 +Código del subsector al que pertenece la entidad pública.
115 +
116 +Tipo: Secuencia de números enteros separados por puntos.
117 +
118 +Ejemplo: `1.10`.
119 +
120 +#### entidad_area
121 +
122 +Código del área al que pertenece la entidad pública.
123 +
124 +Tipo: Secuencia de números enteros separados por puntos.
125 +
126 +Ejemplo: `1.1.1`.
127 +
128 +#### entidad_subarea
129 +
130 +Código de la subárea al que pertenece la entidad pública.
131 +
132 +Tipo: Secuencia de números enteros separados por puntos.
133 +
134 +Ejemplo: `1.1.1.1.1`.
135 +
136 +#### entidad_desc_sector
137 +
138 +Descripción del sector al que pertenece la entidad pública.
139 +
140 +Tipo: Texto descriptivo.
141 +
142 +Ejemplo: `Sector Público`.
143 +
144 +#### entidad_desc_subsector
145 +
146 +Descripción del subsector al que pertenece la entidad pública.
147 +
148 +Tipo: Texto descriptivo.
149 +
150 +Ejemplo: `Sector Público No Financiero`.
151 +
152 +#### entidad_desc_area
153 +
154 +Descripción del área al que pertenece la entidad pública.
155 +
156 +Tipo: Texto descriptivo.
157 +
158 +Ejemplo: `Administraciones Públicas (Gob. General)`.
159 +
160 +#### entidad_desc_subarea
161 +
162 +Descripción de la subárea a la que pertenece la entidad pública.
163 +
164 +Tipo: Texto descriptivo.
165 +
166 +Ejemplo: `Administración Central`.
167 +
168 +#### entidad_sigla_sector
169 +
170 +Sigla del sector al que pertenece la entidad pública.
171 +
172 +Tipo: Texto corto (sigla).
173 +
174 +Ejemplo: `SP`.
175 +
176 +#### entidad_sigla_subsector
177 +
178 +Sigla del subsector al que pertenece la entidad pública.
179 +
180 +Tipo: Texto corto (sigla).
181 +
182 +Ejemplo: `SPNF`.
183 +
184 +#### entidad_sigla_area
185 +
186 +Sigla del área al que pertenece la entidad pública.
187 +
188 +Tipo: Texto corto (sigla).
189 +
190 +Ejemplo: `GOBGEN`.
191 +
192 +#### entidad_sigla_subarea
193 +
194 +Sigla de la subárea a la que pertenece la entidad pública.
195 +
196 +Tipo: Texto corto (sigla).
197 +
198 +Ejemplo: `ADMCEN`.
199 +
200 +#### da
201 +
202 +Código de la dirección administrativa que registra el gasto dentro de la entidad pública.
203 +
204 +Tipo: Número entero de 1 a 2 dígitos.
205 +
206 +Ejemplo: `1`.
207 +
208 +#### desc_da
209 +
210 +Descripción de la dirección administrativa que registra el gasto dentro de la entidad pública.
211 +
212 +Tipo: Texto descriptivo.
213 +
214 +Ejemplo: `DIRECCION SUPERIOR Y ADMINISTRACION CENTRAL`.
215 +
216 +### Categoría programática
217 +
218 +Información sobre los programas, proyectos y actividades a los que se dedica el gasto. Un programa puede contener múltiples proyectos o actividades.
219 +
220 +```
221 +Programa
222 +├─ Proyecto
223 +└─ Actividad
224 +```
225 +
226 +#### programa
227 +
228 +Código del programa al que se dedica el gasto. Cada programa puede tener múltiples proyectos y/o actividades.
229 +
230 +Tipo: Número entero de 1 a 3 dígitos.
231 +
232 +Ejemplo: `52`.
233 +
234 +#### proyecto
235 +
236 +Código del proyecto al que se dedica el gasto. Este código puede corresponder al código SISIN asignado por el Viceministerio de Inversión Pública y Financiamiento Externo, o un código corto asignado por la entidad pública. Cuando el código es 0, la partida corresponde a una actividad.
237 +
238 +Tipo: Código alfanumérico.
239 +
240 +Ejemplo: `000100000200000`.
241 +
242 +#### actividad
243 +
244 +Código de la actividad a la que se dedica el gasto. Cuando es 0, la partida corresponde a un proyecto.
245 +
246 +Tipo: Número entero de 1 dígito.
247 +
248 +Ejemplo: `0`.
249 +
250 +#### desc_programa
251 +
252 +Descripción del programa al que se dedica el gasto.
253 +
254 +Tipo: Texto descriptivo.
255 +
256 +Ejemplo: `PROYECTO DE FORTALECIMIENTO INSTITUCIONAL - CONSTRUCCIÓN DEL EDIFICIO INTELIGENTE DE LA CANCILLERÍA`.
257 +
258 +#### desc_actividad_proyecto
259 +
260 +Descripción del proyecto o actividad al que se dedica el gasto, según corresponda.
261 +
262 +Tipo: Texto descriptivo.
263 +
264 +Ejemplo: `CONST. EDIFICIO INTELIGENTE MRE LA PAZ`.
265 +
266 +### Financiamiento
267 +
268 +Información sobre el origen del financiamiento para el gasto. Consiste en una fuente de financiamiento, organismo financiador y entidad de transferencia.
269 +
270 +Cada fuente es descrita en 2 niveles: grupo y fuente.
271 +
272 +```
273 +Grupo
274 +└─ Fuente
275 +```
276 +
277 +Cada organismo incluye un código, sigla y descripción. Cada entidad de transferencia consiste en un código de entidad similar a los códigos de las entidades que realizan el gasto.
278 +
279 +#### grupo_fuente
280 +
281 +Código del grupo de la fuente de financiamiento.
282 +
283 +Tipo: Número entero de 1 dígito.
284 +
285 +Ejemplo: `1`.
286 +
287 +#### fuente
288 +
289 +Código de la fuente de financiamiento.
290 +
291 +Tipo: Número entero de 2 dígitos.
292 +
293 +Ejemplo: `10`.
294 +
295 +#### desc_grupo_fuente
296 +
297 +Descripción del grupo de la fuente de financiamiento.
298 +
299 +Tipo: Texto descriptivo.
300 +
301 +Ejemplo: `TESORO GENERAL`.
302 +
303 +#### desc_fuente
304 +
305 +Descripción de la fuente de financiamiento.
306 +
307 +Tipo: Texto descriptivo.
308 +
309 +Ejemplo: `Tesoro General de la Nación`.
310 +
311 +#### organismo
312 +
313 +Código del organismo financiador del gasto.
314 +
315 +Tipo: Número entero de 3 dígitos.
316 +
317 +Ejemplo: `111`.
318 +
319 +#### desc_organismo
320 +
321 +Descripción del organismo financiador.
322 +
323 +Tipo: Texto descriptivo.
324 +
325 +Ejemplo: `Tesoro General de la Nación`.
326 +
327 +#### sigla_organismo
328 +
329 +Sigla del organismo financiador.
330 +
331 +Tipo: Texto corto (sigla).
332 +
333 +Ejemplo: `TGN`.
334 +
335 +#### entidad_transferencia
336 +
337 +Código de la entidad de la que se transfieren recursos de financiamiento cuando corresponde.
338 +
339 +Tipo: Número entero de 1 a 4 dígitos.
340 +
341 +Ejemplo: `2351`.
342 +
343 +### Objeto del gasto
344 +
345 +Información sobre la naturaleza económica del gasto. Cada objeto de gasto es descrito en 4 niveles: grupo, subgrupo, partida y subpartida, y cada nivel cuenta con un código y una descripción.
346 +
347 +```
348 +Grupo
349 +└─ Subgrupo
350 + └─ Partida
351 + └─ Subpartida
352 +```
353 +
354 +#### objeto_grupo
355 +
356 +Código del grupo del objeto de gasto.
357 +
358 +Tipo: Número entero de 1 dígito.
359 +
360 +Ejemplo: `1`.
361 +
362 +#### objeto_subgrupo
363 +
364 +Código del subgrupo del objeto de gasto.
365 +
366 +Tipo: Número entero de 1 dígito.
367 +
368 +Ejemplo: `1`.
369 +
370 +#### objeto_partida
371 +
372 +Código de la partida del objeto de gasto.
373 +
374 +Tipo: Número entero de 1 dígito.
375 +
376 +Ejemplo: `2`.
377 +
378 +#### objeto_subpartida
379 +
380 +Código de la subpartida del objeto de gasto.
381 +
382 +Tipo: Número entero de 2 dígitos.
383 +
384 +Ejemplo: `20`.
385 +
386 +#### objeto_desc_grupo
387 +
388 +Descripción del grupo del objeto de gasto.
389 +
390 +Tipo: Texto descriptivo.
391 +
392 +Ejemplo: `SERVICIOS PERSONALES`.
393 +
394 +#### objeto_desc_subgrupo
395 +
396 +Descripción del subgrupo del objeto de gasto.
397 +
398 +Tipo: Texto descriptivo.
399 +
400 +Ejemplo: `Empleados Permanentes`.
401 +
402 +#### objeto_desc_partida
403 +
404 +Descripción de la partida del objeto de gasto.
405 +
406 +Tipo: Texto descriptivo.
407 +
408 +Ejemplo: `Bono de Antiguedad`.
409 +
410 +#### objeto_desc_subpartida
411 +
412 +Descripción de la subpartida del objeto de gasto.
413 +
414 +Tipo: Texto descriptivo.
415 +
416 +Ejemplo: `Bono de Antigûedad`.
417 +
418 +### Finalidad y función
419 +
420 +Información sobre el propósito público del gasto. Consiste en clasificaciones según la finalidad y función, el sector económico y la ubicación geográfica.
421 +
422 +La finalidad y función es descrita en 3 niveles: finalidad, grupo de función y función.
423 +
424 +```
425 +Finalidad
426 +└─ Grupo de Función
427 + └─ Función
428 +```
429 +
430 +El sector económico describe el sector donde el gasto tiene impacto directo y consiste en 3 niveles: sector, subsector y actividad.
431 +
432 +```
433 +Sector
434 +└─ Subsector
435 + └─ Actividad
436 +```
437 +
438 +La ubicación geográfica describe el lugar donde el gasto tiene impacto directo y tiene 3 niveles: departamento, provincia y municipio.
439 +
440 +```
441 +Departamento
442 +└─ Provincia
443 + └─ Municipio
444 +```
445 +
446 +#### finfun_finalidad
447 +
448 +Código de finalidad del gasto.
449 +
450 +Tipo: Número entero de 1 a 2 dígitos.
451 +
452 +Ejemplo: `8`.
453 +
454 +#### finfun_grpfuncion
455 +
456 +Código del grupo de función del gasto.
457 +
458 +Tipo: Número entero de 1 a 2 dígitos.
459 +
460 +Ejemplo: `2`.
461 +
462 +#### finfun_funcion
463 +
464 +Código de la función del gasto.
465 +
466 +Tipo: Número entero de 1 dígito.
467 +
468 +Ejemplo: `0`.
469 +
470 +#### finfun_desc_finalidad
471 +
472 +Descripción de la finalidad del gasto.
473 +
474 +Tipo: Texto descriptivo.
475 +
476 +Ejemplo: `ACTIVIDADES RECREATIVAS, CULTURA Y RELIGION`.
477 +
478 +#### finfun_desc_grpfuncion
479 +
480 +Descripción del grupo de la función del gasto.
481 +
482 +Tipo: Texto descriptivo.
483 +
484 +Ejemplo: `Servicios culturales`.
485 +
486 +#### finfun_desc_funcion
487 +
488 +Descripción de la función del gasto.
489 +
490 +Tipo: Texto descriptivo.
491 +
492 +Ejemplo: `Servicios culturales`.
493 +
494 +#### acteco_sector
495 +
496 +Código del sector económico donde tiene impacto directo el gasto.
497 +
498 +Tipo: Número entero de 1 a 2 dígitos.
499 +
500 +Ejemplo: `9`.
501 +
502 +#### acteco_subsector
503 +
504 +Código del subsector económico donde tiene impacto directo el gasto.
505 +
506 +Tipo: Número entero de 1 dígito.
507 +
508 +Ejemplo: `4`.
509 +
510 +#### acteco_actividad
511 +
512 +Código de la actividad económica donde tiene impacto directo el gasto.
513 +
514 +Tipo: Número entero de 1 a 2 dígitos.
515 +
516 +Ejemplo: `1`.
517 +
518 +#### acteco_desc_sector
519 +
520 +Descripción del sector económico donde tiene impacto directo el gasto.
521 +
522 +Tipo: Texto descriptivo.
523 +
524 +Ejemplo: `EDUCACION Y CULTURA`.
525 +
526 +#### acteco_desc_subsector
527 +
528 +Descripción del subsector económico donde tiene impacto directo el gasto.
529 +
530 +Tipo: Texto descriptivo.
531 +
532 +Ejemplo: `CULTURA`.
533 +
534 +#### acteco_desc_actividad
535 +
536 +Descripción de la actividad económica donde tiene impacto directo el gasto.
537 +
538 +Tipo: Texto descriptivo.
539 +
540 +Ejemplo: `CONSTRUCCION, REFACCION Y EQUIP. DE CENTROS CULTURALES`.
541 +
542 +#### departamento
543 +
544 +Código del departamento donde tiene impacto directo el gasto.
545 +
546 +Tipo: Número entero de 1 a 2 dígitos.
547 +
548 +Ejemplo: `2`.
549 +
550 +#### provincia
551 +
552 +Código de la provincia donde tiene impacto directo el gasto.
553 +
554 +Tipo: Número entero de 1 a 2 dígitos.
555 +
556 +Ejemplo: `6`.
557 +
558 +#### municipio
559 +
560 +Código del municipio donde tiene impacto directo el gasto.
561 +
562 +Tipo: Número entero de 1 a 2 dígitos.
563 +
564 +Ejemplo: `1`.
565 +
566 +#### desc_departamento
567 +
568 +Descripción del departamento donde tiene impacto directo el gasto.
569 +
570 +Tipo: Texto descriptivo.
571 +
572 +Ejemplo: `LA PAZ`.
573 +
574 +#### desc_provincia
575 +
576 +Descripción de la provincia donde tiene impacto directo el gasto.
577 +
578 +Tipo: Texto descriptivo.
579 +
580 +Ejemplo: `OMASUYOS`.
581 +
582 +#### desc_municipio
583 +
584 +Descripción del municipio donde tiene impacto directo el gasto.
585 +
586 +Tipo: Texto descriptivo.
587 +
588 +Ejemplo: `Achacachi`.
589 +
590 +### Montos
591 +
592 +Montos en bolivianos corrientes que hacen al presupuesto y ejecución de cada gasto. Incluye 2 montos de presupuesto: al inicio de la gestión y luego de modificaciones, y 2 montos de ejecución: devengado y pagado.
593 +
594 +#### inicial
595 +
596 +Monto presupuestado al inicio de la gestión, en bolivianos corrientes.
597 +
598 +Tipo: Monto en bolivianos corrientes con dos decimales.
599 +
600 +Ejemplo: `1411136`.
601 +
602 +#### vigente
603 +
604 +Monto presupuestado luego de modificaciones durante la gestión, en bolivianos corrientes.
605 +
606 +Tipo: Monto en bolivianos corrientes con dos decimales.
607 +
608 +Ejemplo: `514479`.
609 +
610 +#### devengado
611 +
612 +Monto devengado para la ejecución, en bolivianos corrientes.
613 +
614 +Tipo: Monto en bolivianos corrientes con dos decimales.
615 +
616 +Ejemplo: `493141.14`.
617 +
618 +#### pagado
619 +
620 +Monto pagado, en bolivianos corrientes.
621 +
622 +Tipo: Monto en bolivianos corrientes con dos decimales.
623 +
624 +Ejemplo: `493141.14`.
625 +
626 +## Ingresos
627 +
628 +La tabla de ingresos nos permite comprender el origen de los recursos públicos.
629 +
630 +- **Quién**: entidad que registra el ingreso.
631 +- **Qué recursos**: rubros del ingreso.
632 +- **De dónde**: fuentes de financiamiento.
633 +- **Cuánto**: montos de presupuesto y ejecución.
634 +
635 +### Gestión
636 +
637 +#### gestion
638 +
639 +Año fiscal del registro presupuestario.
640 +
641 +Tipo: Número entero de 4 dígitos.
642 +
643 +Ejemplo: `2005`.
644 +
645 +### Clasificador institucional
646 +
647 +Información sobre qué entidad percibe el ingreso. Una entidad ocupa una posición dentro de la jerarquía del aparato estatal:
648 +
649 +```
650 +Sector
651 +└─ Subsector
652 + └─ Área
653 + └─ Subárea
654 + └─ Entidad
655 +```
656 +
657 +#### entidad
658 +
659 +Código de la entidad pública a la que pertenece el registro. Cada entidad ocupa una posición en el Sector Público que puede describirse en una jerarquía de 4 niveles: sector, subsector, área y subárea. Para cada nivel se cuenta con un código, sigla y descripción.
660 +
661 +Tipo: Número entero de 1 a 4 dígitos.
662 +
663 +Ejemplo: `10`.
664 +
665 +#### entidad_desc_entidad
666 +
667 +Descripción de la entidad pública.
668 +
669 +Tipo: Texto descriptivo.
670 +
671 +Ejemplo: `Ministerio De Relaciones Exteriores Y Culto`.
672 +
673 +#### entidad_sigla_entidad
674 +
675 +Sigla de la entidad pública.
676 +
677 +Tipo: Texto corto (sigla).
678 +
679 +Ejemplo: `MIN-RREE`.
680 +
681 +#### entidad_sector
682 +
683 +Código del sector al que pertenece la entidad pública.
684 +
685 +Tipo: Número entero de 1 dígito.
686 +
687 +Ejemplo: `1`.
688 +
689 +#### entidad_subsector
690 +
691 +Código del subsector al que pertenece la entidad pública.
692 +
693 +Tipo: Secuencia de números enteros separados por puntos.
694 +
695 +Ejemplo: `1.10`.
696 +
697 +#### entidad_area
698 +
699 +Código del área al que pertenece la entidad pública.
700 +
701 +Tipo: Secuencia de números enteros separados por puntos.
702 +
703 +Ejemplo: `1.1.1`.
704 +
705 +#### entidad_subarea
706 +
707 +Código de la subárea al que pertenece la entidad pública.
708 +
709 +Tipo: Secuencia de números enteros separados por puntos.
710 +
711 +Ejemplo: `1.1.1.1.1`.
712 +
713 +#### entidad_desc_sector
714 +
715 +Descripción del sector al que pertenece la entidad pública.
716 +
717 +Tipo: Texto descriptivo.
718 +
719 +Ejemplo: `Sector Público`.
720 +
721 +#### entidad_desc_subsector
722 +
723 +Descripción del subsector al que pertenece la entidad pública.
724 +
725 +Tipo: Texto descriptivo.
726 +
727 +Ejemplo: `Sector Público No Financiero`.
728 +
729 +#### entidad_desc_area
730 +
731 +Descripción del área al que pertenece la entidad pública.
732 +
733 +Tipo: Texto descriptivo.
734 +
735 +Ejemplo: `Administraciones Públicas (Gob. General)`.
736 +
737 +#### entidad_desc_subarea
738 +
739 +Descripción de la subárea a la que pertenece la entidad pública.
740 +
741 +Tipo: Texto descriptivo.
742 +
743 +Ejemplo: `Administración Central`.
744 +
745 +#### entidad_sigla_sector
746 +
747 +Sigla del sector al que pertenece la entidad pública.
748 +
749 +Tipo: Texto corto (sigla).
750 +
751 +Ejemplo: `SP`.
752 +
753 +#### entidad_sigla_subsector
754 +
755 +Sigla del subsector al que pertenece la entidad pública.
756 +
757 +Tipo: Texto corto (sigla).
758 +
759 +Ejemplo: `SPNF`.
760 +
761 +#### entidad_sigla_area
762 +
763 +Sigla del área al que pertenece la entidad pública.
764 +
765 +Tipo: Texto corto (sigla).
766 +
767 +Ejemplo: `GOBGEN`.
768 +
769 +#### entidad_sigla_subarea
770 +
771 +Sigla de la subárea a la que pertenece la entidad pública.
772 +
773 +Tipo: Texto corto (sigla).
774 +
775 +Ejemplo: `ADMCEN`.
776 +
777 +### Financiamiento
778 +
779 +Información sobre el origen del financiamiento del ingreso. Consiste en una fuente de financiamiento, un organismo financiador y una entidad otorgante. Cada fuente se describe en 2 niveles: grupo y fuente.
780 +
781 +```
782 +Grupo
783 +└─ Fuente
784 +```
785 +
786 +Cada organismo incluye un código, sigla y descripción. La entidad otorgante se registra con un código de entidad.
787 +
788 +#### grupo_fuente
789 +
790 +Código del grupo de la fuente de financiamiento.
791 +
792 +Tipo: Número entero de 1 dígito.
793 +
794 +Ejemplo: `2`.
795 +
796 +#### fuente
797 +
798 +Código de la fuente de financiamiento.
799 +
800 +Tipo: Número entero de 2 dígitos.
801 +
802 +Ejemplo: `11`.
803 +
804 +#### desc_grupo_fuente
805 +
806 +Descripción del grupo de la fuente de financiamiento.
807 +
808 +Tipo: Texto descriptivo.
809 +
810 +Ejemplo: `T.G.N. OTROS INGRESOS`.
811 +
812 +#### desc_fuente
813 +
814 +Descripción de la fuente de financiamiento.
815 +
816 +Tipo: Texto descriptivo.
817 +
818 +Ejemplo: `T.G.N. Otros Ingresos`.
819 +
820 +#### organismo
821 +
822 +Código del organismo financiador del ingreso.
823 +
824 +Tipo: Número entero de 3 dígitos.
825 +
826 +Ejemplo: `0`.
827 +
828 +#### desc_organismo
829 +
830 +Descripción del organismo financiador.
831 +
832 +Tipo: Texto descriptivo.
833 +
834 +Ejemplo: `NC`.
835 +
836 +#### sigla_organismo
837 +
838 +Sigla del organismo financiador.
839 +
840 +Tipo: Texto corto (sigla).
841 +
842 +Ejemplo: `NC`.
843 +
844 +#### entidad_otorgante
845 +
846 +Código de la entidad de la que se transfieren recursos de financiamiento cuando corresponde.
847 +
848 +Tipo: Número entero de 1 a 4 dígitos.
849 +
850 +Ejemplo: `0`.
851 +
852 +### Rubro del ingreso
853 +
854 +Información sobre el origen de los recursos. Cada rubro se describe en 4 niveles: tipo, clase, cuenta y subcuenta, y cada nivel cuenta con un código y una descripción.
855 +
856 +```
857 +Tipo
858 +└─ Clase
859 + └─ Cuenta
860 + └─ Subcuenta
861 +```
862 +
863 +#### rubro_tipo
864 +
865 +Código del tipo de rubro.
866 +
867 +Tipo: Número entero de 2 dígitos.
868 +
869 +Ejemplo: `18`.
870 +
871 +#### rubro_clase
872 +
873 +Código de la clase de rubro.
874 +
875 +Tipo: Número entero de 1 dígito.
876 +
877 +Ejemplo: `2`.
878 +
879 +#### rubro_cuenta
880 +
881 +Código de la cuenta de rubro.
882 +
883 +Tipo: Número entero de 1 dígito.
884 +
885 +Ejemplo: `1`.
886 +
887 +#### rubro_sub_cuenta
888 +
889 +Código de la subcuenta de rubro.
890 +
891 +Tipo: Número entero de 1 dígito.
892 +
893 +Ejemplo: `1`.
894 +
895 +#### rubro_desc_tipo
896 +
897 +Descripción del tipo de rubro.
898 +
899 +Tipo: Texto descriptivo.
900 +
901 +Ejemplo: `DONACIONES CORRIENTES`.
902 +
903 +#### rubro_desc_clase
904 +
905 +Descripción de la clase de rubro.
906 +
907 +Tipo: Texto descriptivo.
908 +
909 +Ejemplo: `Donaciones Corrientes del Exterior`.
910 +
911 +#### rubro_desc_cuenta
912 +
913 +Descripción de la cuenta de rubro.
914 +
915 +Tipo: Texto descriptivo.
916 +
917 +Ejemplo: `Del Sector Privado del Exterior`.
918 +
919 +#### rubro_desc_sub_cuenta
920 +
921 +Descripción de la subcuenta de rubro.
922 +
923 +Tipo: Texto descriptivo.
924 +
925 +Ejemplo: `Monetizable`.
926 +
927 +### Montos
928 +
929 +Montos en bolivianos corrientes que hacen al presupuesto y ejecución de cada ingreso. Incluye 2 montos de presupuesto: al inicio de la gestión y luego de modificaciones, y 2 montos de ejecución: devengado y percibido.
930 +
931 +#### inicial
932 +
933 +Monto presupuestado al inicio de la gestión, en bolivianos corrientes.
934 +
935 +Tipo: Monto en bolivianos corrientes con dos decimales.
936 +
937 +Ejemplo: `2014000`.
938 +
939 +#### vigente
940 +
941 +Monto presupuestado luego de modificaciones durante la gestión, en bolivianos corrientes.
942 +
943 +Tipo: Monto en bolivianos corrientes con dos decimales.
944 +
945 +Ejemplo: `5653320`.
946 +
947 +#### devengado
948 +
949 +Monto devengado para la ejecución, en bolivianos corrientes.
950 +
951 +Tipo: Monto en bolivianos corrientes con dos decimales.
952 +
953 +Ejemplo: `5637823.65`.
954 +
955 +#### percibido
956 +
957 +Monto percibido, en bolivianos corrientes.
958 +
959 +Tipo: Monto en bolivianos corrientes con dos decimales.
960 +
961 +Ejemplo: `5637823.65`.
962 +
963 +# Origen y calidad
964 +
965 +La calidad de estas bases depende principalmente de los sistemas dónde se registran y procesan. En la historia reciente del presupuesto la información pudo ser registrada de manera central o descentralizada. Un registro central se almacena directamente en un servidor controlado por el Ministerio de Economía, que tiende a facilitar la coherencia, completitud y resguardo de la base. Mientras que un registro descentralizado se almacena en una computadora de la entidad responsable y sólo es comunicado periódicamente al Ministerio. La probabilidad de errores en el llenado, envío, validación y resguardo histórico de registros descentralizados es mucho mayor. Prácticamente toda la dificultad de reconstruir bases históricas recae en cómo integrar registros descentralizados ruidosos con registros centralizados en buen estado. Estos registros son particularmente comunes antes de 2016, cuando el SIGEP, un sistema centralizado, es implementado en entidades del nivel central del Estado.
...\ No newline at end of file ...\ No newline at end of file
1 +<script>
2 + import { scaleLinear, scaleBand } from 'd3-scale';
3 +
4 + let {
5 + data = [],
6 + hoveredYear = $bindable(null),
7 + height = 220,
8 + fill = false,
9 + marginTop = 20,
10 + marginRight = 10,
11 + marginBottom = 30,
12 + marginLeft = 50
13 + } = $props();
14 +
15 + // Medir altura real del contenedor cuando fill=true
16 + let wrapperEl = $state(null);
17 + let measuredHeight = $state(height);
18 +
19 + $effect(() => {
20 + if (fill && wrapperEl) {
21 + const updateHeight = () => {
22 + const h = wrapperEl.clientHeight;
23 + if (h > 0) measuredHeight = h;
24 + };
25 + updateHeight();
26 + const observer = new ResizeObserver(updateHeight);
27 + observer.observe(wrapperEl);
28 + return () => observer.disconnect();
29 + } else {
30 + measuredHeight = height;
31 + }
32 + });
33 +
34 + // Dimensiones del área del gráfico
35 + let effectiveHeight = $derived(fill ? measuredHeight : height);
36 + let chartHeight = $derived(effectiveHeight - marginTop - marginBottom);
37 +
38 + // Escalas D3
39 + let xScale = $derived(
40 + scaleBand()
41 + .domain(data.map(d => d.año))
42 + .range([0, 100]) // porcentaje
43 + .padding(0.2)
44 + );
45 +
46 + // Para CSS bottom positioning: 0 → 0%, max → 100%
47 + let yScale = $derived(
48 + scaleLinear()
49 + .domain([0, Math.max(...data.map(d => d.perCapita))])
50 + .range([0, chartHeight])
51 + .nice()
52 + );
53 +
54 + // Ticks para el eje Y
55 + let yTicks = $derived(yScale.ticks(3));
56 +
57 + // Índices para mostrar en eje X
58 + let midIndex = $derived(Math.floor(data.length / 2));
59 +
60 + // Formato para valores del eje Y
61 + function formatValue(v) {
62 + if (v >= 1e6) return (v / 1e6).toLocaleString('es-BO', { maximumFractionDigits: 1 }) + ' M';
63 + if (v >= 1e3) return Math.round(v / 1e3).toLocaleString('es-BO') + ' K';
64 + return v.toLocaleString('es-BO');
65 + }
66 +</script>
67 +
68 +<div class="chart-wrapper" bind:this={wrapperEl} style="height: {fill ? '100%' : height + 'px'}">
69 + <!-- Contenedor principal con márgenes -->
70 + <div class="chart-inner" style="top: {marginTop}px; bottom: {marginBottom}px; left: {marginLeft}px; right: {marginRight}px">
71 +
72 + <!-- Y-axis labels (fuera del área de barras, a la izquierda) -->
73 + {#each yTicks as tick}
74 + {@const pct = (yScale(tick) / chartHeight) * 100}
75 + <span class="y-label" style="bottom: {pct}%; left: -{marginLeft}px">
76 + Bs {formatValue(tick)}
77 + </span>
78 + {/each}
79 +
80 + <!-- Grid lines -->
81 + {#each yTicks as tick}
82 + {@const pct = (yScale(tick) / chartHeight) * 100}
83 + <div class="grid-line" style="bottom: {pct}%"></div>
84 + {/each}
85 +
86 + <!-- Barras -->
87 + <div class="bars" onmouseleave={() => hoveredYear = null} role="group">
88 + {#each data as d}
89 + {@const barPct = (yScale(d.perCapita) / chartHeight) * 100}
90 + <div
91 + class="bar-container"
92 + onmouseenter={() => hoveredYear = d.año}
93 + role="button"
94 + tabindex="0"
95 + >
96 + <div
97 + class="bar"
98 + class:hovered={hoveredYear === d.año}
99 + style="height: {barPct}%"
100 + ></div>
101 + </div>
102 + {/each}
103 + </div>
104 + </div>
105 +
106 + <!-- Eje X -->
107 + <div class="x-axis" style="height: {marginBottom}px; left: {marginLeft}px; right: {marginRight}px">
108 + {#each data as d, i}
109 + {@const isFirst = i === 0}
110 + {@const isLast = i === data.length - 1}
111 + {@const isMid = i === midIndex && data.length > 5}
112 + {@const showLabel = isFirst || isLast || isMid || hoveredYear === d.año}
113 + <span
114 + class="x-label"
115 + class:visible={showLabel}
116 + >
117 + {d.año}
118 + </span>
119 + {/each}
120 + </div>
121 +</div>
122 +
123 +<style>
124 + .chart-wrapper {
125 + position: relative;
126 + width: 100%;
127 + transition: height 0.3s ease;
128 + }
129 +
130 + .chart-inner {
131 + position: absolute;
132 + }
133 +
134 + .y-label {
135 + position: absolute;
136 + width: 45px;
137 + text-align: right;
138 + transform: translateY(50%);
139 + font-family: 'Qanelas', var(--font-sans);
140 + font-size: 0.6875rem;
141 + color: var(--theme-texto);
142 + white-space: nowrap;
143 + }
144 +
145 + .grid-line {
146 + position: absolute;
147 + left: 0;
148 + right: 0;
149 + border-top: 0.5px dotted var(--theme-texto);
150 + opacity: 0.15;
151 + pointer-events: none;
152 + }
153 +
154 + .bars {
155 + position: absolute;
156 + top: 0;
157 + left: 0;
158 + right: 0;
159 + bottom: 0;
160 + display: flex;
161 + align-items: flex-end;
162 + gap: 2px;
163 + }
164 +
165 + .bar-container {
166 + flex: 1;
167 + height: 100%;
168 + display: flex;
169 + align-items: flex-end;
170 + cursor: default;
171 + }
172 +
173 + .bar {
174 + width: 100%;
175 + background: #c4897d;
176 + border-radius: 4px 4px 0 0;
177 + transition: opacity 0.1s ease;
178 + min-height: 2px;
179 + }
180 +
181 + .bars:hover .bar {
182 + opacity: 0.35;
183 + }
184 +
185 + .bars:hover .bar.hovered {
186 + opacity: 1;
187 + }
188 +
189 + .x-axis {
190 + position: absolute;
191 + bottom: 0;
192 + display: flex;
193 + align-items: center;
194 + }
195 +
196 + .x-label {
197 + flex: 1;
198 + text-align: center;
199 + font-family: 'Qanelas', var(--font-sans);
200 + font-size: 0.6875rem;
201 + color: var(--theme-texto);
202 + opacity: 0;
203 + transition: opacity 0.2s ease;
204 + }
205 +
206 + .x-label.visible {
207 + opacity: 1;
208 + }
209 +</style>
1 +<script>
2 + let {
3 + entidades = [],
4 + selectedEntity = $bindable(null),
5 + nEntidades = 0
6 + } = $props();
7 +
8 + let dropdownOpen = $state(false);
9 + let searchQuery = $state('');
10 + let entityLimit = $state(30);
11 +
12 + let filteredEntities = $derived.by(() => {
13 + if (!searchQuery) return entidades.slice(0, entityLimit);
14 + const q = searchQuery.toLowerCase();
15 + return entidades
16 + .filter(e =>
17 + e.entidad_desc?.toLowerCase().includes(q) ||
18 + e.entidad?.toString().includes(q)
19 + )
20 + .slice(0, entityLimit);
21 + });
22 +
23 + function selectEntity(entity) {
24 + selectedEntity = entity;
25 + dropdownOpen = false;
26 + searchQuery = '';
27 + }
28 +
29 + function clearEntity() {
30 + selectedEntity = null;
31 + dropdownOpen = false;
32 + searchQuery = '';
33 + }
34 +
35 + function handleScroll(e) {
36 + const { scrollTop, scrollHeight, clientHeight } = e.target;
37 + if (scrollTop + clientHeight >= scrollHeight - 50) {
38 + entityLimit += 20;
39 + }
40 + }
41 +</script>
42 +
43 +<div class="entity-selector">
44 + <button class="entity-btn" onclick={() => dropdownOpen = !dropdownOpen}>
45 + <span class="entity-label">
46 + {#if selectedEntity}
47 + {selectedEntity.entidad_desc}
48 + {:else}
49 + Todo el sector público
50 + {/if}
51 + </span>
52 + <svg class="chevron" class:open={dropdownOpen} width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
53 + <path d="M6 9l6 6 6-6"/>
54 + </svg>
55 + </button>
56 +
57 + {#if dropdownOpen}
58 + <div class="entity-dropdown">
59 + <div class="dropdown-search">
60 + <input
61 + type="text"
62 + bind:value={searchQuery}
63 + placeholder="Buscar entidad..."
64 + />
65 + </div>
66 + <button
67 + class="dropdown-item"
68 + class:active={!selectedEntity}
69 + onclick={clearEntity}
70 + >
71 + Todo el sector público
72 + <span class="item-count">({nEntidades})</span>
73 + </button>
74 + <div class="dropdown-list" onscroll={handleScroll}>
75 + {#each filteredEntities as entity}
76 + <button
77 + class="dropdown-item"
78 + class:active={selectedEntity?.entidad === entity.entidad}
79 + onclick={() => selectEntity(entity)}
80 + >
81 + <span class="item-code">{entity.entidad}</span>
82 + <span class="item-name">{entity.entidad_desc}</span>
83 + </button>
84 + {/each}
85 + </div>
86 + </div>
87 + {/if}
88 +</div>
89 +
90 +<style>
91 + .entity-selector {
92 + position: relative;
93 + flex: 1;
94 + min-width: 0;
95 + }
96 +
97 + .entity-btn {
98 + display: inline-flex;
99 + align-items: center;
100 + gap: 0.375rem;
101 + padding: 0.125rem 0;
102 + background: transparent;
103 + border: none;
104 + border-bottom: 1px dotted #333333;
105 + color: var(--theme-titulo);
106 + font-size: 1rem;
107 + font-weight: 500;
108 + cursor: pointer;
109 + transition: border-color 0.2s;
110 + }
111 +
112 + .entity-btn:hover {
113 + border-bottom: 1px dotted #666666;
114 + }
115 +
116 + .entity-label {
117 + text-align: left;
118 + }
119 +
120 + .chevron {
121 + color: var(--theme-texto);
122 + opacity: 0.5;
123 + transition: transform 0.2s;
124 + flex-shrink: 0;
125 + }
126 +
127 + .chevron.open {
128 + transform: rotate(180deg);
129 + }
130 +
131 + .entity-dropdown {
132 + position: absolute;
133 + top: calc(100% + 4px);
134 + left: 0;
135 + min-width: 280px;
136 + background: var(--theme-surface);
137 + border: 1px solid var(--theme-borde);
138 + border-radius: 10px;
139 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
140 + z-index: 100;
141 + overflow: hidden;
142 + }
143 +
144 + .dropdown-search {
145 + padding: 0.5rem;
146 + border-bottom: 1px solid var(--theme-borde);
147 + }
148 +
149 + .dropdown-search input {
150 + width: 100%;
151 + padding: 0.5rem 0.75rem;
152 + border: 1px solid var(--theme-borde);
153 + border-radius: 6px;
154 + background: var(--theme-body);
155 + color: var(--theme-titulo);
156 + font-size: 0.8125rem;
157 + outline: none;
158 + }
159 +
160 + .dropdown-search input:focus {
161 + border-color: #6B9FD4;
162 + }
163 +
164 + .dropdown-item {
165 + display: flex;
166 + align-items: center;
167 + gap: 0.5rem;
168 + width: 100%;
169 + padding: 0.625rem 0.75rem;
170 + border: none;
171 + background: transparent;
172 + color: var(--theme-titulo);
173 + font-size: 0.8125rem;
174 + text-align: left;
175 + cursor: pointer;
176 + transition: background 0.15s;
177 + }
178 +
179 + .dropdown-item:hover {
180 + background: var(--theme-surface-hover);
181 + }
182 +
183 + .dropdown-item.active {
184 + background: rgba(107, 159, 212, 0.1);
185 + color: #6B9FD4;
186 + }
187 +
188 + .item-count {
189 + margin-left: auto;
190 + font-size: 0.75rem;
191 + color: var(--theme-texto);
192 + opacity: 0.6;
193 + }
194 +
195 + .item-code {
196 + font-family: 'DM Mono', monospace;
197 + font-size: 0.6875rem;
198 + color: var(--theme-texto);
199 + min-width: 2rem;
200 + }
201 +
202 + .item-name {
203 + flex: 1;
204 + overflow: hidden;
205 + text-overflow: ellipsis;
206 + white-space: nowrap;
207 + }
208 +
209 + .dropdown-list {
210 + max-height: 240px;
211 + overflow-y: auto;
212 + }
213 +
214 + :global(html.dark) .dropdown-item.active {
215 + background: rgba(201, 167, 81, 0.1);
216 + color: var(--theme-accent);
217 + }
218 +</style>
1 +<script>
2 + import { goto } from '$app/navigation';
3 + import {
4 + query,
5 + results,
6 + isLoading as searchLoading,
7 + indexLoaded,
8 + selectedIndex,
9 + performSearch,
10 + clearSearch,
11 + navigateResults
12 + } from '$lib/stores/searchStore';
13 +
14 + let searchInputRef = $state(null);
15 + let searchVal = $state('');
16 + let searchFocused = $state(false);
17 + let debounceTimer;
18 +
19 + // Filtrar solo objeto_gasto
20 + let filteredSearchResults = $derived($results.filter(r => r.tipo === 'objeto_gasto'));
21 +
22 + function handleSearchInput(e) {
23 + const val = e.target.value;
24 + clearTimeout(debounceTimer);
25 + debounceTimer = setTimeout(() => {
26 + if (val.length >= 2) {
27 + performSearch(val);
28 + } else {
29 + clearSearch();
30 + }
31 + }, 200);
32 + }
33 +
34 + function handleSearchKeydown(e) {
35 + if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
36 + e.preventDefault();
37 + navigateResults(e.key === 'ArrowDown' ? 1 : -1, filteredSearchResults.length);
38 + } else if (e.key === 'Enter' && filteredSearchResults.length > 0) {
39 + e.preventDefault();
40 + const selected = filteredSearchResults[$selectedIndex];
41 + if (selected) handleSearchSelect(selected);
42 + } else if (e.key === 'Escape') {
43 + searchVal = '';
44 + clearSearch();
45 + searchInputRef?.blur();
46 + }
47 + }
48 +
49 + function handleSearchSelect(result) {
50 + goto(`/objeto/${result.codigo}`);
51 + searchVal = '';
52 + clearSearch();
53 + searchFocused = false;
54 + }
55 +
56 + function handleSearchBlur() {
57 + setTimeout(() => {
58 + searchFocused = false;
59 + }, 150);
60 + }
61 +
62 + function handleClear() {
63 + clearSearch();
64 + searchVal = '';
65 + }
66 +</script>
67 +
68 +<div class="objeto-search" class:focused={searchFocused}>
69 + <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
70 + <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
71 + </svg>
72 + <input
73 + type="text"
74 + bind:this={searchInputRef}
75 + bind:value={searchVal}
76 + oninput={handleSearchInput}
77 + onkeydown={handleSearchKeydown}
78 + onfocus={() => searchFocused = true}
79 + onblur={handleSearchBlur}
80 + placeholder="Buscar objeto..."
81 + disabled={!$indexLoaded}
82 + />
83 + {#if searchVal}
84 + <button class="search-clear" onclick={handleClear}>
85 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
86 + <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
87 + </svg>
88 + </button>
89 + {/if}
90 + {#if searchFocused && searchVal.length >= 2}
91 + <div class="search-dropdown">
92 + {#if $searchLoading}
93 + <div class="search-msg">Buscando...</div>
94 + {:else if filteredSearchResults.length === 0}
95 + <div class="search-msg">Sin resultados</div>
96 + {:else}
97 + {#each filteredSearchResults as result, i}
98 + <button
99 + class="search-item"
100 + class:selected={$selectedIndex === i}
101 + onclick={() => handleSearchSelect(result)}
102 + onmouseenter={() => selectedIndex.set(i)}
103 + >
104 + <span class="item-code">{result.codigo}</span>
105 + <span class="item-name">{result.nombre}</span>
106 + </button>
107 + {/each}
108 + {/if}
109 + </div>
110 + {/if}
111 +</div>
112 +
113 +<style>
114 + .objeto-search {
115 + position: relative;
116 + display: flex;
117 + align-items: center;
118 + gap: 0.5rem;
119 + background: #f5f5f5;
120 + border: none;
121 + border-radius: 8px;
122 + padding: 0.625rem 1rem;
123 + transition: box-shadow 0.2s;
124 + width: 100%;
125 + }
126 +
127 + .objeto-search.focused {
128 + border-color: #6B9FD4;
129 + box-shadow: 0 0 0 3px rgba(107, 159, 212, 0.15);
130 + }
131 +
132 + .search-icon {
133 + color: var(--theme-texto);
134 + opacity: 0.5;
135 + flex-shrink: 0;
136 + }
137 +
138 + input {
139 + flex: 1;
140 + border: none;
141 + background: transparent;
142 + color: var(--theme-titulo);
143 + font-size: 0.8125rem;
144 + outline: none;
145 + min-width: 120px;
146 + }
147 +
148 + input::placeholder {
149 + color: var(--theme-texto);
150 + opacity: 0.5;
151 + }
152 +
153 + .search-clear {
154 + display: flex;
155 + align-items: center;
156 + justify-content: center;
157 + padding: 0.25rem;
158 + border-radius: 4px;
159 + border: none;
160 + background: transparent;
161 + color: var(--theme-texto);
162 + cursor: pointer;
163 + transition: background 0.2s;
164 + }
165 +
166 + .search-clear:hover {
167 + background: var(--theme-borde);
168 + }
169 +
170 + .search-dropdown {
171 + position: absolute;
172 + top: calc(100% + 4px);
173 + left: 0;
174 + right: 0;
175 + background: var(--theme-surface);
176 + border: 1px solid var(--theme-borde);
177 + border-radius: 8px;
178 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
179 + max-height: 280px;
180 + overflow-y: auto;
181 + z-index: 100;
182 + }
183 +
184 + .search-msg {
185 + padding: 0.75rem 1rem;
186 + font-size: 0.8125rem;
187 + color: var(--theme-texto);
188 + opacity: 0.7;
189 + }
190 +
191 + .search-item {
192 + display: flex;
193 + align-items: center;
194 + gap: 0.75rem;
195 + width: 100%;
196 + padding: 0.625rem 1rem;
197 + border: none;
198 + background: transparent;
199 + text-align: left;
200 + cursor: pointer;
201 + transition: background 0.15s;
202 + }
203 +
204 + .search-item:hover,
205 + .search-item.selected {
206 + background: var(--theme-surface-hover);
207 + }
208 +
209 + .item-code {
210 + font-family: 'DM Mono', monospace;
211 + font-size: 0.75rem;
212 + color: var(--theme-texto);
213 + min-width: 3.5rem;
214 + }
215 +
216 + .item-name {
217 + font-size: 0.8125rem;
218 + color: var(--theme-titulo);
219 + }
220 +</style>
1 +<script>
2 + let { vista = $bindable('agregado') } = $props();
3 +
4 + function toggle() {
5 + vista = vista === 'agregado' ? 'comparar' : 'agregado';
6 + }
7 +</script>
8 +
9 +<button class="vista-btn" onclick={toggle}>
10 + {#if vista === 'agregado'}
11 + Comparar entre entidades
12 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
13 + <path d="M5 12h14M12 5l7 7-7 7"/>
14 + </svg>
15 + {:else}
16 + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
17 + <path d="M19 12H5M12 19l-7-7 7-7"/>
18 + </svg>
19 + Ver todo
20 + {/if}
21 +</button>
22 +
23 +<style>
24 + .vista-btn {
25 + display: inline-flex;
26 + align-items: center;
27 + gap: 0.375rem;
28 + font-family: 'Qanelas', var(--font-sans);
29 + font-size: 0.75rem;
30 + font-weight: 500;
31 + padding: 0;
32 + border: none;
33 + background: transparent;
34 + color: #666666;
35 + cursor: pointer;
36 + transition: color 0.15s ease;
37 + white-space: nowrap;
38 + text-decoration: underline dotted;
39 + text-underline-offset: 3px;
40 + text-decoration-color: #666666;
41 + }
42 +
43 + .vista-btn svg {
44 + flex-shrink: 0;
45 + opacity: 0.7;
46 + }
47 +
48 + .vista-btn:hover {
49 + color: #999999;
50 + text-decoration-color: #666666;
51 + }
52 +
53 + .vista-btn:hover svg {
54 + opacity: 1;
55 + }
56 +
57 + .vista-btn:active {
58 + color: #555555;
59 + }
60 +</style>
...@@ -60,13 +60,25 @@ ...@@ -60,13 +60,25 @@
60 </div> 60 </div>
61 {/if} 61 {/if}
62 62
63 -<div class="min-h-screen" style="background-color: var(--theme-body);"> 63 +<div class="layout-wrapper">
64 - <main> 64 + <main class="layout-main">
65 {@render children()} 65 {@render children()}
66 </main> 66 </main>
67 </div> 67 </div>
68 68
69 <style> 69 <style>
70 + .layout-wrapper {
71 + min-height: 100vh;
72 + width: 100%;
73 + background-color: var(--theme-body);
74 + }
75 +
76 + .layout-main {
77 + width: 100%;
78 + max-width: 100%;
79 + overflow-x: hidden;
80 + }
81 +
70 .nav-loader { 82 .nav-loader {
71 position: fixed; 83 position: fixed;
72 top: 0; 84 top: 0;
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
37 let gearMenuOpen = false; 37 let gearMenuOpen = false;
38 let searchInput; 38 let searchInput;
39 let debounceTimer; 39 let debounceTimer;
40 + let autoFocused = false;
40 41
41 // Search filters - which types to include 42 // Search filters - which types to include
42 let searchFilters = { 43 let searchFilters = {
...@@ -68,6 +69,14 @@ ...@@ -68,6 +69,14 @@
68 warmupSupabase(); 69 warmupSupabase();
69 }); 70 });
70 71
72 + // Auto-focus en el buscador cuando el índice esté listo (solo una vez)
73 + $: if ($indexLoaded && searchInput && !autoFocused) {
74 + autoFocused = true;
75 + setTimeout(() => {
76 + searchInput?.focus();
77 + }, 50);
78 + }
79 +
71 function toggleMobileSettings() { 80 function toggleMobileSettings() {
72 mobileSettingsOpen = !mobileSettingsOpen; 81 mobileSettingsOpen = !mobileSettingsOpen;
73 } 82 }
...@@ -124,8 +133,12 @@ ...@@ -124,8 +133,12 @@
124 break; 133 break;
125 case 'Enter': 134 case 'Enter':
126 e.preventDefault(); 135 e.preventDefault();
136 + // Si hay un resultado seleccionado, navegar a él
127 if ($selectedIndex >= 0 && filteredResults[$selectedIndex]) { 137 if ($selectedIndex >= 0 && filteredResults[$selectedIndex]) {
128 handleSelect(filteredResults[$selectedIndex]); 138 handleSelect(filteredResults[$selectedIndex]);
139 + } else if (filteredResults.length > 0) {
140 + // Si no hay selección pero hay resultados, ir al primero
141 + handleSelect(filteredResults[0]);
129 } 142 }
130 break; 143 break;
131 case 'Escape': 144 case 'Escape':
...@@ -323,11 +336,11 @@ ...@@ -323,11 +336,11 @@
323 on:blur={handleBlur} 336 on:blur={handleBlur}
324 disabled={!$indexLoaded} 337 disabled={!$indexLoaded}
325 /> 338 />
326 - {#if !searchVal && !searchFocused} 339 + {#if !searchVal}
327 <div class="search-placeholder"> 340 <div class="search-placeholder">
328 <span class="blink-cursor" style="background:{accent}"></span> 341 <span class="blink-cursor" style="background:{accent}"></span>
329 - <span class="placeholder-text placeholder-desktop">{$indexLoaded ? placeholder : 'Cargando...'}</span> 342 + <span class="placeholder-text placeholder-desktop">{placeholder}</span>
330 - <span class="placeholder-text placeholder-mobile">{$indexLoaded ? (isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...') : 'Cargando...'}</span> 343 + <span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span>
331 </div> 344 </div>
332 {/if} 345 {/if}
333 {#if searchVal && !$isLoading} 346 {#if searchVal && !$isLoading}
......
...@@ -16,19 +16,6 @@ ...@@ -16,19 +16,6 @@
16 let highlightedItem = $state(null); 16 let highlightedItem = $state(null);
17 let sidebarOpen = $state(false); 17 let sidebarOpen = $state(false);
18 18
19 - // Modo embed (cuando está dentro de un iframe en /objeto/[codigo])
20 - let isEmbed = $derived($page.url.searchParams.get('embed') === 'true');
21 - let initialObjeto = $derived($page.url.searchParams.get('objeto'));
22 -
23 - // Función para navegar a un objeto (en modo embed envía mensaje al padre)
24 - function navigateToObjeto(codigo) {
25 - if (isEmbed && window.parent !== window) {
26 - window.parent.postMessage({ type: 'navigate-objeto', codigo }, '*');
27 - } else {
28 - goto(`/objeto/${codigo}`);
29 - }
30 - }
31 -
32 // Modo de visualización desde URL 19 // Modo de visualización desde URL
33 let viewMode = $derived($page.url.searchParams.get('modo') || 'lista'); 20 let viewMode = $derived($page.url.searchParams.get('modo') || 'lista');
34 21
...@@ -1459,7 +1446,6 @@ ...@@ -1459,7 +1446,6 @@
1459 class="transition-colors hover:text-[var(--theme-accent)]" 1446 class="transition-colors hover:text-[var(--theme-accent)]"
1460 style="color: var(--theme-texto);" 1447 style="color: var(--theme-texto);"
1461 title="Ver detalle" 1448 title="Ver detalle"
1462 - onclick={(e) => { if (isEmbed) { e.preventDefault(); navigateToObjeto(selectedGrupo.objeto); }}}
1463 > 1449 >
1464 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1450 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1465 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> 1451 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
...@@ -1510,7 +1496,6 @@ ...@@ -1510,7 +1496,6 @@
1510 class="transition-colors hover:text-[var(--theme-accent)]" 1496 class="transition-colors hover:text-[var(--theme-accent)]"
1511 style="color: var(--theme-texto);" 1497 style="color: var(--theme-texto);"
1512 title="Ver página de detalle" 1498 title="Ver página de detalle"
1513 - onclick={(e) => { if (isEmbed) { e.preventDefault(); navigateToObjeto(subgrupo.objeto); }}}
1514 > 1499 >
1515 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1500 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1516 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> 1501 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
...@@ -1557,7 +1542,6 @@ ...@@ -1557,7 +1542,6 @@
1557 class="transition-colors hover:text-[var(--theme-accent)]" 1542 class="transition-colors hover:text-[var(--theme-accent)]"
1558 style="color: var(--theme-texto);" 1543 style="color: var(--theme-texto);"
1559 title="Ver página de detalle" 1544 title="Ver página de detalle"
1560 - onclick={(e) => { if (isEmbed) { e.preventDefault(); navigateToObjeto(partida.objeto); }}}
1561 > 1545 >
1562 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1546 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1563 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> 1547 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
...@@ -1600,7 +1584,6 @@ ...@@ -1600,7 +1584,6 @@
1600 class="transition-colors hover:text-[var(--theme-accent)]" 1584 class="transition-colors hover:text-[var(--theme-accent)]"
1601 style="color: var(--theme-texto);" 1585 style="color: var(--theme-texto);"
1602 title="Ver página de detalle" 1586 title="Ver página de detalle"
1603 - onclick={(e) => { if (isEmbed) { e.preventDefault(); navigateToObjeto(subpartida.objeto); }}}
1604 > 1587 >
1605 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1588 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1606 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /> 1589 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
......
...@@ -2,9 +2,14 @@ ...@@ -2,9 +2,14 @@
2 import { onMount } from 'svelte'; 2 import { onMount } from 'svelte';
3 import { tweened } from 'svelte/motion'; 3 import { tweened } from 'svelte/motion';
4 import { cubicOut } from 'svelte/easing'; 4 import { cubicOut } from 'svelte/easing';
5 - import { supabase } from '$lib/supabase';
6 import { goto } from '$app/navigation'; 5 import { goto } from '$app/navigation';
6 + import { supabase } from '$lib/supabase';
7 import Spinner from '$lib/components/ui/Spinner.svelte'; 7 import Spinner from '$lib/components/ui/Spinner.svelte';
8 + import ObjetoSearch from '$lib/components/objeto/ObjetoSearch.svelte';
9 + import VistaToggle from '$lib/components/objeto/VistaToggle.svelte';
10 + import EntitySelector from '$lib/components/objeto/EntitySelector.svelte';
11 + import BarChart from '$lib/components/objeto/BarChart.svelte';
12 + import { initIndex } from '$lib/stores/searchStore';
8 13
9 // Datos del servidor (reactivos) 14 // Datos del servidor (reactivos)
10 let { data } = $props(); 15 let { data } = $props();
...@@ -23,9 +28,12 @@ ...@@ -23,9 +28,12 @@
23 let loading = $state(true); 28 let loading = $state(true);
24 let hoveredYear = $state(null); 29 let hoveredYear = $state(null);
25 let showInfo = $state(false); 30 let showInfo = $state(false);
31 + let showDefinicionesModal = $state(false);
32 + let showMobileSidebar = $state(false);
26 let vista = $state('agregado'); // 'agregado' | 'comparar' 33 let vista = $state('agregado'); // 'agregado' | 'comparar'
27 let isDark = $state(true); 34 let isDark = $state(true);
28 35
36 +
29 // Parsear descripciones del objeto 37 // Parsear descripciones del objeto
30 function parseDescripciones(descripcionesStr) { 38 function parseDescripciones(descripcionesStr) {
31 if (!descripcionesStr) return []; 39 if (!descripcionesStr) return [];
...@@ -82,8 +90,41 @@ ...@@ -82,8 +90,41 @@
82 return labels[nivel] || nivel; 90 return labels[nivel] || nivel;
83 } 91 }
84 92
93 + function getNivelPlural(nivel) {
94 + const labels = { grupo: 'grupos', subgrupo: 'subgrupos', partida: 'partidas', subpartida: 'subpartidas' };
95 + return labels[nivel] || nivel;
96 + }
97 +
98 + function getNivelGastado(nivel) {
99 + const gastado = { grupo: 'gastado', subgrupo: 'gastado', partida: 'gastada', subpartida: 'gastada' };
100 + return gastado[nivel] || 'gastado';
101 + }
102 +
103 + function getNivelSingular(nivel) {
104 + const labels = { grupo: 'grupo', subgrupo: 'subgrupo', partida: 'partida', subpartida: 'subpartida' };
105 + return labels[nivel] || nivel;
106 + }
107 +
108 + function getNivelArticulo(nivel) {
109 + const articulos = { grupo: 'el', subgrupo: 'el', partida: 'la', subpartida: 'la' };
110 + return articulos[nivel] || 'el';
111 + }
112 +
113 + function getNivelArticuloContracto(nivel) {
114 + const articulos = { grupo: 'del', subgrupo: 'del', partida: 'de la', subpartida: 'de la' };
115 + return articulos[nivel] || 'del';
116 + }
117 +
118 + function getHijosNivelLabel(nivel) {
119 + const labels = { grupo: 'subgrupos', subgrupo: 'partidas', partida: 'subpartidas' };
120 + return labels[nivel] || '';
121 + }
122 +
85 let descripciones = $derived(parseDescripciones(objetoData.descripciones)); 123 let descripciones = $derived(parseDescripciones(objetoData.descripciones));
86 124
125 + // Texto narrativo para el nivel
126 + let nivelNarrativo = $derived(`${getNivelArticulo(objetoData.nivel)} ${getNivelLabel(objetoData.nivel)} de Gasto`);
127 +
87 // Info del objeto (reactivo) 128 // Info del objeto (reactivo)
88 let objeto = $derived({ 129 let objeto = $derived({
89 codigo: objetoData.objeto, 130 codigo: objetoData.objeto,
...@@ -153,31 +194,6 @@ ...@@ -153,31 +194,6 @@
153 } 194 }
154 195
155 // ══════════════════════════════════════════════════════════════ 196 // ══════════════════════════════════════════════════════════════
156 - // SPLIT VIEW - CLASIFICADOR LADO A LADO
157 - // ══════════════════════════════════════════════════════════════
158 - let splitOpen = $state(false);
159 - let splitWidth = $state(45); // porcentaje del ancho para el clasificador
160 - let isResizing = $state(false);
161 -
162 - function startResize(e) {
163 - isResizing = true;
164 - document.body.style.cursor = 'col-resize';
165 - document.body.style.userSelect = 'none';
166 - }
167 -
168 - function handleResize(e) {
169 - if (!isResizing) return;
170 - const newWidth = (e.clientX / window.innerWidth) * 100;
171 - splitWidth = Math.max(25, Math.min(65, newWidth)); // entre 25% y 65%
172 - }
173 -
174 - function stopResize() {
175 - isResizing = false;
176 - document.body.style.cursor = '';
177 - document.body.style.userSelect = '';
178 - }
179 -
180 - // ══════════════════════════════════════════════════════════════
181 // DATOS DE SUPABASE 197 // DATOS DE SUPABASE
182 // ══════════════════════════════════════════════════════════════ 198 // ══════════════════════════════════════════════════════════════
183 let datosAnuales = $state([]); // Datos por año 199 let datosAnuales = $state([]); // Datos por año
...@@ -344,21 +360,21 @@ ...@@ -344,21 +360,21 @@
344 ); 360 );
345 361
346 let displayMonto = $derived(currentData ? currentData.monto : totalHistorico); 362 let displayMonto = $derived(currentData ? currentData.monto : totalHistorico);
347 - let displayMontoLabel = $derived(currentData ? `gasto en ${currentData.año}` : `gasto ${primerAño}-${ultimoAño}`); 363 + let displayMontoLabel = $derived(currentData ? `gastados en ${currentData.año}` : `gastados ${primerAño}-${ultimoAño}`);
348 let displayPerCapita = $derived(currentData ? currentData.perCapita : promedioPerCapita); 364 let displayPerCapita = $derived(currentData ? currentData.perCapita : promedioPerCapita);
349 - let displayPerCapitaLabel = $derived(currentData ? `por persona en ${currentData.año}` : 'por persona (promedio)'); 365 + let displayPerCapitaLabel = $derived(currentData ? `por cada boliviano en ${currentData.año}` : 'por cada boliviano');
350 let displayPorcentaje = $derived( 366 let displayPorcentaje = $derived(
351 currentData && totalHistorico > 0 367 currentData && totalHistorico > 0
352 ? (totalPorcentaje * (currentData.monto / (totalHistorico / gastoPerCapita.length))).toFixed(1) 368 ? (totalPorcentaje * (currentData.monto / (totalHistorico / gastoPerCapita.length))).toFixed(1)
353 : totalPorcentaje 369 : totalPorcentaje
354 ); 370 );
355 - let displayPorcentajeLabel = $derived(currentData ? `del gasto ${currentData.año}` : 'del gasto total'); 371 + let displayPorcentajeLabel = $derived(currentData ? `del gasto en ${currentData.año}` : 'del gasto total');
356 let displayPeriodo = $derived(currentData ? currentData.año : `${primerAño}-${ultimoAño}`); 372 let displayPeriodo = $derived(currentData ? currentData.año : `${primerAño}-${ultimoAño}`);
357 let displayRanking = $derived(`${displayRankingValue} de ${rankingDenominador}`); 373 let displayRanking = $derived(`${displayRankingValue} de ${rankingDenominador}`);
358 let displayRankingLabel = $derived( 374 let displayRankingLabel = $derived(
359 selectedEntity 375 selectedEntity
360 ? (currentData ? `entre las que más gastan en ${currentData.año}` : 'entre las que más gastan') 376 ? (currentData ? `entre las que más gastan en ${currentData.año}` : 'entre las que más gastan')
361 - : (currentData ? `ranking en ${currentData.año}` : 'ranking entre grupos') 377 + : (currentData ? `${getNivelPlural(objetoData.nivel)} más ${getNivelGastado(objetoData.nivel)} en ${currentData.año}` : `${getNivelPlural(objetoData.nivel)} más ${getNivelGastado(objetoData.nivel)}`)
362 ); 378 );
363 379
364 // Top entidades reactivo según hover 380 // Top entidades reactivo según hover
...@@ -642,6 +658,9 @@ ...@@ -642,6 +658,9 @@
642 const mountStart = performance.now(); 658 const mountStart = performance.now();
643 setTimeout(() => { mounted = true; }, 50); 659 setTimeout(() => { mounted = true; }, 50);
644 660
661 + // Inicializar índice de búsqueda
662 + initIndex();
663 +
645 // Leer estado del tema 664 // Leer estado del tema
646 isDark = document.documentElement.classList.contains('dark'); 665 isDark = document.documentElement.classList.contains('dark');
647 666
...@@ -660,7 +679,7 @@ ...@@ -660,7 +679,7 @@
660 console.log(`[CLIENT] Promise.all: ${(performance.now() - loadStart).toFixed(0)}ms`); 679 console.log(`[CLIENT] Promise.all: ${(performance.now() - loadStart).toFixed(0)}ms`);
661 console.log(`[CLIENT] onMount total: ${(performance.now() - mountStart).toFixed(0)}ms`); 680 console.log(`[CLIENT] onMount total: ${(performance.now() - mountStart).toFixed(0)}ms`);
662 681
663 - // Cerrar dropdown al hacer click fuera 682 + // Cerrar dropdowns al hacer click fuera
664 const handleClickOutside = (e) => { 683 const handleClickOutside = (e) => {
665 if (entityDropdownOpen && !e.target.closest('.entity-dropdown-container')) { 684 if (entityDropdownOpen && !e.target.closest('.entity-dropdown-container')) {
666 entityDropdownOpen = false; 685 entityDropdownOpen = false;
...@@ -675,24 +694,9 @@ ...@@ -675,24 +694,9 @@
675 }; 694 };
676 document.addEventListener('click', handleClickOutside); 695 document.addEventListener('click', handleClickOutside);
677 696
678 - // Resize handlers para split view
679 - document.addEventListener('mousemove', handleResize);
680 - document.addEventListener('mouseup', stopResize);
681 -
682 - // Escuchar mensajes del iframe (clasificador en modo embed)
683 - const handleIframeMessage = (e) => {
684 - if (e.data?.type === 'navigate-objeto' && e.data?.codigo) {
685 - goto(`/objeto/${e.data.codigo}`);
686 - }
687 - };
688 - window.addEventListener('message', handleIframeMessage);
689 -
690 return () => { 697 return () => {
691 observer.disconnect(); 698 observer.disconnect();
692 document.removeEventListener('click', handleClickOutside); 699 document.removeEventListener('click', handleClickOutside);
693 - document.removeEventListener('mousemove', handleResize);
694 - document.removeEventListener('mouseup', stopResize);
695 - window.removeEventListener('message', handleIframeMessage);
696 }; 700 };
697 }); 701 });
698 702
...@@ -714,20 +718,29 @@ ...@@ -714,20 +718,29 @@
714 } 718 }
715 }); 719 });
716 720
721 + // Recargar datos cuando cambia la entidad seleccionada
722 + let previousEntity = selectedEntity;
723 + $effect(() => {
724 + if (mounted && selectedEntity !== previousEntity) {
725 + previousEntity = selectedEntity;
726 + loadData();
727 + }
728 + });
729 +
717 // ══════════════════════════════════════════════════════════════ 730 // ══════════════════════════════════════════════════════════════
718 // HELPERS 731 // HELPERS
719 // ══════════════════════════════════════════════════════════════ 732 // ══════════════════════════════════════════════════════════════
720 function formatMonto(m) { 733 function formatMonto(m) {
721 - if (m >= 1e9) return (m / 1e9).toFixed(1) + 'MM'; 734 + if (m >= 1e9) return (m / 1e9).toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' MM';
722 - if (m >= 1e6) return (m / 1e6).toFixed(1) + 'M'; 735 + if (m >= 1e6) return (m / 1e6).toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' M';
723 - if (m >= 1e3) return Math.round(m / 1e3) + 'K'; 736 + if (m >= 1e3) return Math.round(m / 1e3).toLocaleString('es-BO') + ' K';
724 - return m?.toLocaleString() || '0'; 737 + return m?.toLocaleString('es-BO') || '0';
725 } 738 }
726 739
727 function formatMontoLargo(m) { 740 function formatMontoLargo(m) {
728 - if (m >= 1e9) return Math.round(m / 1e9).toLocaleString('es-BO') + ' mil Mill.'; 741 + if (m >= 1e9) return Math.round(m / 1e9).toLocaleString('es-BO') + ' mil mill. de Bs.';
729 - if (m >= 1e6) return (m / 1e6).toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' Mill.'; 742 + if (m >= 1e6) return (m / 1e6).toLocaleString('es-BO', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' mill. de Bs.';
730 - return m?.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0'; 743 + return m?.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' Bs.' || '0';
731 } 744 }
732 745
733 function formatPerCapita(n) { 746 function formatPerCapita(n) {
...@@ -744,100 +757,79 @@ ...@@ -744,100 +757,79 @@
744 <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" /> 757 <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=DM+Mono:wght@400;500&family=Instrument+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
745 </svelte:head> 758 </svelte:head>
746 759
747 -<!-- Contenedor split view --> 760 +<div class="page" class:mounted>
748 -<div class="split-container" class:split-active={splitOpen}> 761 + <!-- Link a clasificadores -->
749 - <!-- Panel izquierdo: Clasificador --> 762 + <nav class="page-nav">
750 - {#if splitOpen} 763 + <a href="/clasificadores/objeto-gasto" class="back-link">
751 - <div class="split-left" style="width: {splitWidth}%"> 764 + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
752 - <div class="split-header"> 765 + <path d="M19 12H5M12 19l-7-7 7-7"/>
753 - <span>Clasificador por Objeto</span>
754 - <button class="split-close" onclick={() => splitOpen = false}>
755 - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
756 - <path d="M18 6L6 18M6 6l12 12"/>
757 </svg> 766 </svg>
758 - </button> 767 + Clasificador
759 - </div> 768 + </a>
760 - <iframe
761 - src="/clasificadores/objeto-gasto?embed=true&objeto={objetoCodigo}"
762 - title="Clasificador"
763 - class="split-iframe"
764 - ></iframe>
765 - </div>
766 -
767 - <!-- Resizer -->
768 - <div
769 - class="split-resizer"
770 - onmousedown={startResize}
771 - role="separator"
772 - ></div>
773 - {/if}
774 -
775 - <!-- Panel derecho: Contenido principal -->
776 - <div class="split-right" class:mounted>
777 - <!-- Header compacto -->
778 - <header>
779 - <nav class="breadcrumb">
780 - <a href="/clasificadores/objeto-gasto">← Objeto del Gasto</a>
781 - {#each padres as padre}
782 - <span class="breadcrumb-sep">/</span>
783 - <a href="/objeto/{padre.objeto}" class="breadcrumb-link">{padre.desc_objeto}</a>
784 - {/each}
785 </nav> 769 </nav>
786 770
787 - <div class="title-row"> 771 + <!-- DASHBOARD LAYOUT -->
788 - <div class="title-main"> 772 + {#if vista === 'agregado'}
789 - <span class="codigo">{objeto.codigo}</span> 773 + <div class="dashboard-layout">
790 - <h1> 774 + <!-- CONTEXTO IZQUIERDO -->
791 - {objeto.nombre} 775 + <aside class="dashboard-context" class:mobile-open={showMobileSidebar}>
792 - <button class="help-icon" onclick={() => showInfo = !showInfo} class:active={showInfo}> 776 + <!-- Botón móvil para abrir/cerrar -->
793 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 777 + <button class="mobile-sidebar-toggle" onclick={() => showMobileSidebar = !showMobileSidebar}>
794 - <circle cx="12" cy="12" r="10"/> 778 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
795 - <path d="M9.5 9a3 3 0 1 1 3.5 2.95c-.5.17-1 .62-1 1.3V15"/> 779 + <line x1="4" y1="21" x2="4" y2="14"/>
796 - <circle cx="12" cy="18" r="0.5" fill="currentColor"/> 780 + <line x1="4" y1="10" x2="4" y2="3"/>
781 + <line x1="12" y1="21" x2="12" y2="12"/>
782 + <line x1="12" y1="8" x2="12" y2="3"/>
783 + <line x1="20" y1="21" x2="20" y2="16"/>
784 + <line x1="20" y1="12" x2="20" y2="3"/>
785 + <circle cx="4" cy="12" r="2"/>
786 + <circle cx="12" cy="10" r="2"/>
787 + <circle cx="20" cy="14" r="2"/>
797 </svg> 788 </svg>
789 + <span class="toggle-label">{showMobileSidebar ? 'Ocultar' : 'Herramientas'}</span>
798 </button> 790 </button>
799 - </h1>
800 - </div>
801 - </div>
802 -
803 - <div class="meta">
804 - <span class="badge">{objeto.nivel}</span>
805 - <span class="years">{formatYearsAsRanges(objeto.años)}</span>
806 - </div>
807 - </header>
808 791
809 - <!-- Panel de info (colapsable) --> 792 + <div class="context-card">
810 - {#if showInfo} 793 + <!-- Título principal -->
811 - <div class="info-panel"> 794 + <h2 class="context-main-title">Herramientas</h2>
812 - <div class="info-content"> 795 +
813 - <h4>Definición vigente</h4> 796 + <!-- Buscador -->
814 - <p>{objeto.descripcion}</p> 797 + <div class="context-section">
798 + <h3 class="context-subtitle">Buscar</h3>
799 + <p class="context-hint">Buscar otro grupo, subgrupo, partida o subpartida</p>
800 + <ObjetoSearch />
801 + </div>
802 +
803 + <!-- Separador -->
804 + <div class="context-separator"></div>
805 +
806 + <!-- Contexto: Árbol de jerarquía -->
807 + {#if padres.length > 0 || hijos.length > 0}
808 + <div class="context-section">
809 + <h3 class="context-subtitle">Navegación</h3>
810 + <p class="context-hint">Partidas relacionadas. <span class="desktop-hint">Haz click</span><span class="mobile-hint">Toca</span> para explorar.</p>
811 + <div class="tree">
812 + {#each padres as padre, i}
813 + <a href="/objeto/{padre.objeto}" class="tree-node has-tooltip" style="--indent: {i}" data-tooltip="{padre.desc_objeto}">
814 + <span class="tree-dot"></span>
815 + <span class="tree-code">{padre.objeto}</span>
816 + <span class="tree-name">{padre.desc_objeto}</span>
817 + </a>
818 + {/each}
815 819
816 - {#if objeto.variaciones.length > 1} 820 + <div class="tree-node tree-node-current has-tooltip" style="--indent: {padres.length}" data-tooltip="{objetoData.desc_objeto}">
817 - <h4>Variaciones históricas</h4> 821 + <span class="tree-dot"></span>
818 - {#each objeto.variaciones as v} 822 + <span class="tree-code">{objetoData.objeto}</span>
819 - <div class="variacion"> 823 + <span class="tree-name">{objetoData.desc_objeto}</span>
820 - <span class="variacion-rango">{formatYearsAsRanges(v.rango)}</span>
821 - <p>{v.texto}</p>
822 </div> 824 </div>
823 - {/each}
824 - {/if}
825 825
826 {#if hijos.length > 0} 826 {#if hijos.length > 0}
827 - <h4> 827 + <div class="tree-children" style="--indent: {padres.length + 1}">
828 - {#if objetoNivel === 'grupo'}
829 - Subgrupos ({hijos.length})
830 - {:else if objetoNivel === 'subgrupo'}
831 - Partidas ({hijos.length})
832 - {:else if objetoNivel === 'partida'}
833 - Subpartidas ({hijos.length})
834 - {/if}
835 - </h4>
836 - <div class="hijos-list">
837 {#each hijos as hijo} 828 {#each hijos as hijo}
838 - <a href="/objeto/{hijo.objeto}" class="hijo-link"> 829 + <a href="/objeto/{hijo.objeto}" class="tree-node tree-node-child has-tooltip" data-tooltip="{hijo.desc_objeto}">
839 - <span class="hijo-codigo">{hijo.objeto}</span> 830 + <span class="tree-dot"></span>
840 - <span class="hijo-nombre">{hijo.desc_objeto}</span> 831 + <span class="tree-code">{hijo.objeto}</span>
832 + <span class="tree-name">{hijo.desc_objeto}</span>
841 </a> 833 </a>
842 {/each} 834 {/each}
843 </div> 835 </div>
...@@ -846,184 +838,165 @@ ...@@ -846,184 +838,165 @@
846 </div> 838 </div>
847 {/if} 839 {/if}
848 840
849 - <!-- Dashboard principal --> 841 + <!-- Separador -->
850 - <main> 842 + <div class="context-separator"></div>
851 843
852 - <!-- Toggle de vista --> 844 + <!-- Contexto: Vigencia temporal -->
853 - <div class="vista-toggle"> 845 + <div class="context-section">
854 - <button 846 + <h3 class="context-subtitle">Comparabilidad</h3>
855 - class="toggle-btn" 847 + <div class="vigencia-compact">
856 - class:active={vista === 'agregado'} 848 + {#if gastoPerCapita.length === 21 && primerAño === 2005 && ultimoAño === 2025}
857 - onclick={() => vista = 'agregado'} 849 + <div class="vigencia-status-icon vigencia-full">
858 - > 850 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
859 - Agregado 851 + <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
860 - </button> 852 + <polyline points="22 4 12 14.01 9 11.01"/>
861 - <button 853 + </svg>
862 - class="toggle-btn" 854 + </div>
863 - class:active={vista === 'comparar'} 855 + <div class="vigencia-text">
864 - onclick={() => vista = 'comparar'} 856 + <span class="vigencia-main">Comparable en todo el período</span>
865 - > 857 + <span class="vigencia-sub">{primerAño} – {ultimoAño}</span>
866 - Comparar
867 - </button>
868 </div> 858 </div>
869 -
870 - <!-- Selector de entidad (solo en vista agregado) -->
871 - {#if vista === 'agregado'}
872 - <div class="entity-selector entity-dropdown-container">
873 - <span class="selector-label">Viendo</span>
874 - <div class="relative">
875 - <button
876 - class="selector-btn"
877 - onclick={() => entityDropdownOpen = !entityDropdownOpen}
878 - >
879 - <span class="truncate">
880 - {#if selectedEntity}
881 - {selectedEntity.entidad_desc?.slice(0, 30)}{selectedEntity.entidad_desc?.length > 30 ? '...' : ''}
882 {:else} 859 {:else}
883 - Todo el sector público 860 + <div class="vigencia-status-icon vigencia-partial">
884 - {/if} 861 + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
885 - </span> 862 + <circle cx="12" cy="12" r="10"/>
886 - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 863 + <line x1="12" y1="8" x2="12" y2="12"/>
887 - <path d="M6 9l6 6 6-6"/> 864 + <line x1="12" y1="16" x2="12.01" y2="16"/>
888 </svg> 865 </svg>
889 - </button>
890 -
891 - {#if entityDropdownOpen}
892 - <div class="dropdown-panel">
893 - <div class="dropdown-search">
894 - <input
895 - type="text"
896 - bind:value={entitySearchQuery}
897 - placeholder="Buscar entidad..."
898 - class="search-input"
899 - />
900 </div> 866 </div>
901 - <button 867 + <div class="vigencia-text">
902 - class="dropdown-item" 868 + <span class="vigencia-main">Comparable en {gastoPerCapita.length} años</span>
903 - class:active={!selectedEntity} 869 + <span class="vigencia-sub">{primerAño} – {ultimoAño}</span>
904 - onclick={() => clearEntity()}
905 - >
906 - <span class="font-medium">Todo el sector público</span>
907 - <span class="text-xs opacity-60 ml-2">({nEntidades} entidades)</span>
908 - </button>
909 - <div class="dropdown-list" onscroll={(e) => handleEntityScroll(e, 'main')}>
910 - {#each filteredEntities as entity}
911 - <button
912 - class="dropdown-item"
913 - class:active={selectedEntity?.entidad === entity.entidad}
914 - onclick={() => selectEntity(entity)}
915 - >
916 - <span class="entity-code">{entity.entidad}</span>
917 - <span class="truncate">{entity.entidad_desc}</span>
918 - </button>
919 - {/each}
920 - {#if entityLimit < entidades.length}
921 - <div class="text-center py-2 text-xs opacity-60">
922 - Scroll para ver más ({entidades.length - entityLimit} restantes)
923 </div> 870 </div>
924 {/if} 871 {/if}
925 </div> 872 </div>
926 </div> 873 </div>
874 +
875 + <!-- Separador -->
876 + <div class="context-separator"></div>
877 +
878 + <!-- Contexto: Definición oficial -->
879 + <div class="context-section">
880 + <h3 class="context-subtitle">Definición</h3>
881 + <p class="context-desc">Descripción oficial {getNivelArticuloContracto(objetoData.nivel)} {objetoData.nivel} según el clasificador presupuestario.</p>
882 + <div class="definicion-compact">
883 + {#if objeto.variaciones.length === 1}
884 + <span class="def-status">Sin cambios en el período</span>
885 + {:else}
886 + <span class="def-status def-changed">{objeto.variaciones.length} versiones en el período</span>
927 {/if} 887 {/if}
888 + <button class="def-btn" onclick={() => showDefinicionesModal = true}>
889 + Ver
890 + </button>
928 </div> 891 </div>
929 - <span class="interaction-hint desktop-hint">Pasa el cursor sobre las barras para ver valores por año</span>
930 - <span class="interaction-hint mobile-hint">Toca las barras para ver valores por año</span>
931 </div> 892 </div>
932 - {/if} 893 + </div>
894 + </aside>
933 895
934 - {#if vista === 'agregado'} 896 + <!-- CONTENIDO PRINCIPAL -->
935 - <!-- VISTA AGREGADO --> 897 + <main class="dashboard-main" class:expanded={selectedEntity}>
898 + <!-- Gráfico principal con KPIs integrados -->
899 + <div class="chart-section chart-protagonista" class:expanded={selectedEntity}>
900 + <!-- Header: pills + botón -->
901 + <div class="chart-top-row">
902 + <div class="main-title-pills">
903 + <span class="pill pill-nivel">{objeto.nivel}</span>
904 + <span class="pill pill-codigo">{objeto.codigo}</span>
905 + </div>
906 + <VistaToggle bind:vista />
907 + </div>
908 +
909 + <!-- Título principal -->
910 + <h2 class="main-title-name">{objeto.nombre}</h2>
911 +
912 + <!-- Selector de entidad -->
913 + <div class="chart-controls">
914 + <EntitySelector {entidades} bind:selectedEntity {nEntidades} />
915 + </div>
936 916
937 - <!-- KPIs arriba --> 917 + <!-- Título del gráfico -->
918 + <div class="chart-header">
919 + <span class="chart-title">Bs/habitante <span class="chart-period">· {displayPeriodo}</span></span>
920 + </div>
921 + <BarChart data={gastoPerCapita} bind:hoveredYear height={280} fill={!!selectedEntity} />
922 +
923 + <!-- KPIs abajo -->
938 <div class="kpis"> 924 <div class="kpis">
939 <div class="kpi"> 925 <div class="kpi">
940 - <span class="kpi-value">Bs {formatMontoLargo(Math.round($twMonto))}</span> 926 + <span class="kpi-value">{formatMontoLargo(Math.round($twMonto))}</span>
941 <span class="kpi-label">{displayMontoLabel}</span> 927 <span class="kpi-label">{displayMontoLabel}</span>
942 </div> 928 </div>
943 -
944 <div class="kpi"> 929 <div class="kpi">
945 - <span class="kpi-value">Bs {formatPerCapita($twPerCapita)}</span> 930 + <span class="kpi-value">{formatPerCapita($twPerCapita)} Bs.</span>
946 <span class="kpi-label">{displayPerCapitaLabel}</span> 931 <span class="kpi-label">{displayPerCapitaLabel}</span>
947 </div> 932 </div>
948 -
949 <div class="kpi"> 933 <div class="kpi">
950 <span class="kpi-value">{$twPorcentaje.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span> 934 <span class="kpi-value">{$twPorcentaje.toLocaleString('es-BO', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}%</span>
951 <span class="kpi-label">{displayPorcentajeLabel}</span> 935 <span class="kpi-label">{displayPorcentajeLabel}</span>
952 </div> 936 </div>
953 -
954 <div class="kpi"> 937 <div class="kpi">
955 <span class="kpi-value">{displayRanking}</span> 938 <span class="kpi-value">{displayRanking}</span>
956 <span class="kpi-label">{displayRankingLabel}</span> 939 <span class="kpi-label">{displayRankingLabel}</span>
957 </div> 940 </div>
958 </div> 941 </div>
959 -
960 - <!-- Gráfico de barras (per cápita) - protagonista al medio -->
961 - <div class="chart-section chart-protagonista">
962 - <div class="chart-header">
963 - <span class="chart-title">Gasto por persona</span>
964 - <span class="chart-period">{displayPeriodo}</span>
965 - </div>
966 -
967 - <div class="chart-wrapper">
968 - <!-- Y-axis labels -->
969 - <div class="y-axis">
970 - <span class="y-label">Bs {formatMonto(maxPerCapita)}</span>
971 - <span class="y-label">Bs {formatMonto(Math.round(maxPerCapita / 2))}</span>
972 - <span class="y-label">Bs 0</span>
973 </div> 942 </div>
974 943
975 - <div 944 + <!-- Ranking simplificado -->
976 - class="chart" 945 + {#if !selectedEntity && displayTopEntidades.length > 0}
977 - onmouseleave={() => hoveredYear = null} 946 + <div class="ranking-section">
978 - role="group" 947 + <h3 class="ranking-title">Concentración del gasto</h3>
979 - > 948 + <p class="ranking-desc">Porcentaje del total gastado en {objeto.nombre.toLowerCase()} por todo el Estado {hoveredYear ? `en ${hoveredYear}` : `(${primerAño}-${ultimoAño})`}</p>
980 - {#each gastoPerCapita as g} 949 + <div class="ranking-cards">
981 - <div 950 + {#each displayTopEntidades as ent, i}
982 - class="bar-container" 951 + {@const pct = i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3}
983 - onmouseenter={() => hoveredYear = g.año} 952 + <div class="ranking-card">
984 - role="button" 953 + <span class="ranking-position">#{i + 1}</span>
985 - tabindex="0" 954 + <span class="ranking-name">{ent.nombre}</span>
986 - > 955 + <span class="ranking-pct">{pct.toFixed(1)}%</span>
987 - <div
988 - class="bar"
989 - class:hovered={hoveredYear === g.año}
990 - style="height: {(g.perCapita / maxPerCapita) * 100}%"
991 - ></div>
992 - <span class="bar-label" class:visible={hoveredYear === g.año || g.año % 5 === 0}>
993 - {g.año.toString().slice(-2)}
994 - </span>
995 </div> 956 </div>
996 {/each} 957 {/each}
997 </div> 958 </div>
998 </div> 959 </div>
960 + {/if}
961 + </main>
999 </div> 962 </div>
1000 963
1001 - <!-- Top entidades (solo visible cuando es "Todo el Estado") --> 964 + <!-- Modal de definiciones (fuera del layout) -->
1002 - {#if !selectedEntity} 965 + {#if showDefinicionesModal}
1003 - <div class="top-section"> 966 + <div class="modal-overlay" onclick={() => showDefinicionesModal = false}>
1004 - <div class="top-header"> 967 + <div class="modal-content" onclick={(e) => e.stopPropagation()}>
1005 - <h3>Mayores ejecutores</h3> 968 + <div class="modal-header">
1006 - <span class="top-context">{totalEntidades} entidades · {displayTopLabel}</span> 969 + <h3>Definiciones de "{objetoData.desc_objeto}"</h3>
1007 - </div> 970 + <button class="modal-close" onclick={() => showDefinicionesModal = false}>
1008 - 971 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1009 - <div class="top-list"> 972 + <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
1010 - {#each displayTopEntidades as ent, i (ent.nombre)} 973 + </svg>
1011 - <div class="top-item"> 974 + </button>
1012 - <span class="top-rank">#{i + 1}</span>
1013 - <div class="top-info">
1014 - <span class="top-name">{ent.nombre}</span>
1015 - <div class="top-bar-bg">
1016 - <div class="top-bar" style="width: {((i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3) / $twTop1) * 100}%"></div>
1017 </div> 975 </div>
976 + <div class="modal-body">
977 + {#each objeto.variaciones as v, i}
978 + <div class="def-item" class:def-current={i === 0}>
979 + <div class="def-header">
980 + <span class="def-period">{v.rango}</span>
981 + {#if i === 0}
982 + <span class="def-badge">Vigente</span>
983 + {/if}
1018 </div> 984 </div>
1019 - <span class="top-pct">{(i === 0 ? $twTop1 : i === 1 ? $twTop2 : $twTop3).toFixed(1)}%</span> 985 + <p class="def-text">{v.texto}</p>
1020 </div> 986 </div>
1021 {/each} 987 {/each}
1022 </div> 988 </div>
1023 </div> 989 </div>
990 + </div>
1024 {/if} 991 {/if}
1025 992
1026 {:else} 993 {:else}
994 + <!-- VISTA COMPARAR (mantiene layout original) -->
995 + <div class="toolbar">
996 + <ObjetoSearch />
997 + <VistaToggle bind:vista />
998 + </div>
999 + <main>
1027 <!-- VISTA COMPARAR --> 1000 <!-- VISTA COMPARAR -->
1028 1001
1029 <!-- Selectores de entidades con búsqueda --> 1002 <!-- Selectores de entidades con búsqueda -->
...@@ -1139,7 +1112,7 @@ ...@@ -1139,7 +1112,7 @@
1139 <!-- Gráfico comparativo --> 1112 <!-- Gráfico comparativo -->
1140 <div class="chart-section"> 1113 <div class="chart-section">
1141 <div class="chart-header"> 1114 <div class="chart-header">
1142 - <span class="chart-title">Gasto por persona</span> 1115 + <span class="chart-title">Bs/habitante</span>
1143 <span class="chart-period">{displayPeriodoComparar}</span> 1116 <span class="chart-period">{displayPeriodoComparar}</span>
1144 </div> 1117 </div>
1145 1118
...@@ -1221,354 +1194,845 @@ ...@@ -1221,354 +1194,845 @@
1221 </div> 1194 </div>
1222 {/if} 1195 {/if}
1223 1196
1224 - {/if}
1225 -
1226 </main> 1197 </main>
1227 - </div><!-- end split-right --> 1198 + {/if}
1228 - 1199 +</div>
1229 - <!-- Botón para abrir split view -->
1230 - <button
1231 - class="split-toggle"
1232 - onclick={() => splitOpen = !splitOpen}
1233 - title={splitOpen ? "Cerrar clasificador" : "Abrir clasificador"}
1234 - class:active={splitOpen}
1235 - >
1236 - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1237 - <rect x="3" y="3" width="7" height="18" rx="1"/>
1238 - <rect x="14" y="3" width="7" height="18" rx="1"/>
1239 - </svg>
1240 - <span class="split-toggle-label">{splitOpen ? 'Cerrar' : 'Clasificador'}</span>
1241 - </button>
1242 -</div><!-- end split-container -->
1243 1200
1244 <style> 1201 <style>
1245 - /* Split container */ 1202 + /* ═══════════════════════════════════════════════════════════════
1246 - .split-container { 1203 + PAGE & HEADER - Modern Compact Design
1247 - display: flex; 1204 + ═══════════════════════════════════════════════════════════════ */
1205 + .page {
1248 min-height: 100vh; 1206 min-height: 100vh;
1207 + width: 100%;
1208 + max-width: 100vw;
1209 + overflow-x: hidden;
1249 background: var(--theme-body); 1210 background: var(--theme-body);
1211 + color: var(--theme-titulo);
1212 + font-family: var(--font-sans), -apple-system, sans-serif;
1213 + opacity: 0;
1214 + transition: opacity 0.4s ease;
1250 } 1215 }
1216 + .page.mounted { opacity: 1; }
1251 1217
1252 - .split-left { 1218 + /* Fondo claro específico para objeto */
1253 - flex-shrink: 0; 1219 + :global(html:not(.dark)) .page {
1220 + background: #f5f5f7;
1221 + }
1222 +
1223 + /* ═══════════════════════════════════════════════════════════════
1224 + DASHBOARD LAYOUT - Sidebar + Main Content
1225 + ═══════════════════════════════════════════════════════════════ */
1226 + .dashboard-layout {
1227 + display: grid;
1228 + grid-template-columns: 320px minmax(auto, 900px);
1229 + gap: 16px;
1230 + padding: 1rem 2rem 2rem;
1231 + max-width: 1600px;
1232 + margin: 0 auto;
1233 + justify-content: center;
1234 + }
1235 +
1236 + /* Contexto */
1237 + .dashboard-context {
1254 display: flex; 1238 display: flex;
1255 flex-direction: column; 1239 flex-direction: column;
1256 - height: 100vh; 1240 + min-height: 0;
1257 - position: sticky; 1241 + }
1258 - top: 0; 1242 +
1259 - border-right: 1px solid var(--theme-borde); 1243 + /* Mobile sidebar toggle - oculto en desktop */
1244 + .mobile-sidebar-toggle {
1245 + display: none;
1260 } 1246 }
1261 1247
1262 - .split-right { 1248 + .context-card {
1249 + background: var(--theme-surface);
1250 + border-radius: 16px;
1251 + padding: 1.25rem 1.5rem;
1252 + display: flex;
1253 + flex-direction: column;
1254 + gap: 1rem;
1263 flex: 1; 1255 flex: 1;
1264 - min-width: 0; 1256 + min-height: 0;
1265 - background: var(--theme-body); 1257 + }
1258 +
1259 + .context-main-title {
1260 + font-size: 1.375rem;
1261 + font-weight: 600;
1266 color: var(--theme-titulo); 1262 color: var(--theme-titulo);
1267 - font-family: var(--font-sans), -apple-system, sans-serif; 1263 + margin: 0;
1268 - opacity: 0; 1264 + line-height: 1.2;
1269 - transition: opacity 0.4s ease;
1270 } 1265 }
1271 - .split-right.mounted { opacity: 1; }
1272 1266
1273 - /* Header */ 1267 + .context-subtitle {
1274 - header { 1268 + font-family: 'Qanelas', var(--font-sans);
1275 - padding: 1rem 2rem; 1269 + font-size: 0.875rem;
1276 - border-bottom: 1px solid var(--theme-borde); 1270 + font-weight: 600;
1271 + color: var(--theme-titulo);
1272 + margin: 0;
1277 } 1273 }
1278 1274
1279 - .breadcrumb { 1275 + .context-desc {
1280 - display: flex; 1276 + font-size: 0.75rem;
1281 - align-items: center; 1277 + color: #666666;
1282 - gap: 0.5rem; 1278 + margin: 0.375rem 0 0.5rem;
1283 - flex-wrap: wrap; 1279 + line-height: 1.4;
1284 } 1280 }
1285 1281
1286 - .breadcrumb a { 1282 + .context-hint {
1287 - font-family: 'DM Mono', monospace;
1288 font-size: 0.75rem; 1283 font-size: 0.75rem;
1289 - color: var(--theme-texto); 1284 + color: #666666;
1290 - text-decoration: none; 1285 + margin: 0;
1291 - transition: color 0.2s; 1286 + line-height: 1.4;
1292 } 1287 }
1293 - .breadcrumb a:hover { color: var(--theme-titulo); }
1294 1288
1295 - .breadcrumb-sep { 1289 + .context-label {
1296 - color: var(--theme-borde); 1290 + font-family: 'Qanelas', var(--font-sans);
1297 font-size: 0.75rem; 1291 font-size: 0.75rem;
1292 + font-weight: 600;
1293 + color: var(--theme-texto);
1294 + text-transform: uppercase;
1295 + letter-spacing: 0.03em;
1296 + margin: 0;
1298 } 1297 }
1299 1298
1300 - .breadcrumb-link { 1299 + .desktop-hint {
1301 - max-width: 150px; 1300 + display: inline;
1302 - overflow: hidden;
1303 - text-overflow: ellipsis;
1304 - white-space: nowrap;
1305 } 1301 }
1306 1302
1307 - .title-row { 1303 + .mobile-hint {
1308 - display: flex; 1304 + display: none;
1309 - align-items: flex-start; 1305 + }
1310 - justify-content: space-between; 1306 +
1311 - margin-top: 0.5rem; 1307 + @media (max-width: 768px) {
1312 - gap: 1rem; 1308 + .desktop-hint {
1309 + display: none;
1310 + }
1311 + .mobile-hint {
1312 + display: inline;
1313 + }
1313 } 1314 }
1314 1315
1315 - .title-main { 1316 + .context-section {
1316 display: flex; 1317 display: flex;
1317 - align-items: baseline; 1318 + flex-direction: column;
1318 - gap: 1rem; 1319 + gap: 0.5rem;
1319 - flex-wrap: wrap;
1320 } 1320 }
1321 1321
1322 - .codigo { 1322 + .context-separator {
1323 - font-family: 'DM Mono', monospace; 1323 + border-top: 1px solid var(--theme-borde);
1324 - font-size: 1.5rem; 1324 + margin: 0.25rem 0;
1325 - color: var(--theme-texto);
1326 - font-weight: 400;
1327 } 1325 }
1328 1326
1329 - h1 { 1327 + .sidebar-card {
1330 - font-family: var(--font-display), Georgia, serif; 1328 + background: var(--theme-surface);
1331 - font-size: 1.5rem; 1329 + border-radius: 12px;
1332 - font-weight: 400; 1330 + padding: 1rem;
1333 - color: var(--theme-titulo);
1334 - margin: 0;
1335 - display: inline-flex;
1336 - align-items: center;
1337 - gap: 0.5rem;
1338 } 1331 }
1339 1332
1340 - .help-icon { 1333 + .sidebar-card-header {
1341 - background: transparent; 1334 + font-family: 'DM Mono', monospace;
1342 - border: none; 1335 + font-size: 0.5625rem;
1343 - padding: 0; 1336 + letter-spacing: 0.1em;
1344 color: var(--theme-texto); 1337 color: var(--theme-texto);
1345 - cursor: pointer; 1338 + opacity: 0.6;
1346 - transition: all 0.2s; 1339 + margin-bottom: 0.75rem;
1347 - display: inline-flex; 1340 + text-transform: uppercase;
1348 - align-items: center;
1349 - justify-content: center;
1350 - vertical-align: middle;
1351 } 1341 }
1352 - .help-icon:hover, .help-icon.active { 1342 +
1353 - color: var(--theme-accent); 1343 + .sidebar-separator {
1344 + height: 1px;
1345 + background: var(--theme-borde);
1346 + margin: 0.5rem 0;
1354 } 1347 }
1355 1348
1356 - .meta { 1349 + .sidebar-section {
1357 display: flex; 1350 display: flex;
1358 - align-items: center; 1351 + flex-direction: column;
1359 gap: 0.75rem; 1352 gap: 0.75rem;
1360 - margin-top: 0.5rem;
1361 } 1353 }
1362 1354
1363 - .badge { 1355 + .sidebar-selectors {
1364 - font-family: 'DM Mono', monospace; 1356 + display: flex;
1365 - font-size: 0.625rem; 1357 + flex-direction: column;
1366 - letter-spacing: 0.1em; 1358 + gap: 0.5rem;
1367 - text-transform: uppercase;
1368 - padding: 0.25rem 0.5rem;
1369 - background: var(--theme-surface);
1370 - border: 1px solid var(--theme-borde);
1371 - border-radius: 4px;
1372 - color: var(--theme-texto);
1373 } 1359 }
1374 1360
1375 - .years { 1361 + /* Tree compacto para sidebar */
1376 - font-family: 'DM Mono', monospace; 1362 + .dashboard-context .tree {
1377 font-size: 0.75rem; 1363 font-size: 0.75rem;
1378 - color: var(--theme-texto); 1364 + line-height: 1.6;
1365 + }
1366 +
1367 + .dashboard-context .tree-node {
1368 + padding: 0.375rem 0.5rem;
1369 + margin-left: calc(var(--indent, 0) * 1rem);
1370 + border-left-width: 2px;
1371 + border-radius: 0 6px 6px 0;
1379 } 1372 }
1380 1373
1381 - /* Split View - Header y controles */ 1374 + .dashboard-context .tree-name {
1382 - .split-header { 1375 + font-size: 0.75rem;
1376 + white-space: nowrap;
1377 + overflow: hidden;
1378 + text-overflow: ellipsis;
1379 + max-width: 140px;
1380 + }
1381 +
1382 + .dashboard-context .tree-code {
1383 + font-size: 0.6875rem;
1384 + min-width: 2.5rem;
1385 + }
1386 +
1387 + .dashboard-context .tree-children {
1388 + margin-left: calc(var(--indent, 0) * 1rem);
1389 + }
1390 +
1391 + .dashboard-context .tree-node-child {
1392 + padding-left: 0.75rem;
1393 + }
1394 +
1395 + /* Vigencia compacta */
1396 + .vigencia-compact {
1383 display: flex; 1397 display: flex;
1384 align-items: center; 1398 align-items: center;
1385 - justify-content: space-between; 1399 + gap: 0.75rem;
1386 - padding: 0.75rem 1rem;
1387 - background: var(--theme-surface);
1388 - border-bottom: 1px solid var(--theme-borde);
1389 - font-size: 0.75rem;
1390 - font-weight: 600;
1391 - color: var(--theme-titulo);
1392 } 1400 }
1393 1401
1394 - .split-close { 1402 + .vigencia-status-icon {
1395 - padding: 0.25rem; 1403 + width: 32px;
1396 - border: none; 1404 + height: 32px;
1397 - background: transparent; 1405 + border-radius: 8px;
1398 - color: var(--theme-texto);
1399 - cursor: pointer;
1400 - border-radius: 4px;
1401 display: flex; 1406 display: flex;
1402 align-items: center; 1407 align-items: center;
1403 justify-content: center; 1408 justify-content: center;
1409 + flex-shrink: 0;
1404 } 1410 }
1405 1411
1406 - .split-close:hover { 1412 + .vigencia-status-icon.vigencia-full {
1407 - background: var(--theme-borde); 1413 + background: rgba(74, 158, 107, 0.15);
1414 + color: #4A9E6B;
1415 + }
1416 +
1417 + .vigencia-status-icon.vigencia-partial {
1418 + background: rgba(212, 137, 61, 0.15);
1419 + color: #D4893D;
1420 + }
1421 +
1422 + .vigencia-text {
1423 + display: flex;
1424 + flex-direction: column;
1425 + gap: 0.125rem;
1426 + }
1427 +
1428 + .vigencia-main {
1429 + font-size: 0.875rem;
1430 + font-weight: 500;
1408 color: var(--theme-titulo); 1431 color: var(--theme-titulo);
1409 } 1432 }
1410 1433
1411 - .split-iframe { 1434 + .vigencia-sub {
1412 - flex: 1; 1435 + font-size: 0.6875rem;
1413 - width: 100%; 1436 + color: var(--theme-texto);
1414 - border: none; 1437 + opacity: 0.7;
1415 - background: var(--theme-body);
1416 } 1438 }
1417 1439
1418 - .split-resizer { 1440 + /* Definición compacta */
1419 - width: 8px; 1441 + .definicion-compact {
1420 - background: var(--theme-borde); 1442 + display: flex;
1421 - cursor: col-resize; 1443 + align-items: center;
1422 - flex-shrink: 0; 1444 + justify-content: space-between;
1423 - position: relative; 1445 + gap: 0.5rem;
1424 - transition: background 0.2s;
1425 } 1446 }
1426 1447
1427 - .split-resizer::after { 1448 + .def-status {
1428 - content: ''; 1449 + font-size: 0.8125rem;
1429 - position: absolute; 1450 + color: #555555;
1430 - left: 50%; 1451 + }
1431 - top: 50%; 1452 +
1432 - transform: translate(-50%, -50%); 1453 + .def-status.def-changed {
1433 - width: 4px; 1454 + color: #555555;
1434 - height: 48px; 1455 + }
1435 - background: var(--theme-texto); 1456 +
1457 + .def-btn {
1458 + font-size: 0.6875rem;
1459 + font-weight: 500;
1460 + color: #555555;
1461 + background: rgba(0, 0, 0, 0.06);
1462 + border: none;
1463 + padding: 0.25rem 0.625rem;
1436 border-radius: 4px; 1464 border-radius: 4px;
1437 - opacity: 0.3; 1465 + cursor: pointer;
1438 - transition: opacity 0.2s, height 0.2s; 1466 + transition: background 0.2s;
1439 } 1467 }
1440 1468
1441 - .split-resizer:hover { 1469 + .def-btn:hover {
1442 - background: var(--theme-accent); 1470 + background: rgba(0, 0, 0, 0.1);
1443 } 1471 }
1444 1472
1445 - .split-resizer:hover::after { 1473 + /* Main content */
1446 - opacity: 0.8; 1474 + .dashboard-main {
1447 - height: 64px; 1475 + display: flex;
1476 + flex-direction: column;
1477 + gap: 0.75rem;
1478 + min-width: 0;
1479 + width: 100%;
1480 + align-self: start;
1481 + padding-top: 0;
1482 + padding-bottom: 0;
1483 + transition: all 0.3s ease;
1448 } 1484 }
1449 1485
1450 - /* Botón para abrir/cerrar split view */ 1486 + .dashboard-main.expanded {
1451 - .split-toggle { 1487 + align-self: stretch;
1452 - position: fixed; 1488 + }
1453 - left: 1rem; 1489 +
1454 - bottom: 1.5rem; 1490 + /* Header compacto */
1455 - display: none; 1491 + .page-nav {
1492 + padding: 1rem 2rem;
1493 + }
1494 +
1495 + .back-link {
1496 + display: inline-flex;
1456 align-items: center; 1497 align-items: center;
1457 - gap: 0.5rem; 1498 + gap: 0.375rem;
1458 - padding: 0.625rem 1rem; 1499 + font-family: 'DM Mono', monospace;
1500 + font-size: 0.6875rem;
1501 + color: var(--theme-texto);
1502 + text-decoration: none;
1503 + transition: color 0.2s;
1504 + }
1505 + .back-link:hover { color: var(--theme-accent); }
1506 +
1507 + /* ═══════════════════════════════════════════════════════════════
1508 + TOOLBAR - Barra de acciones moderna
1509 + ═══════════════════════════════════════════════════════════════ */
1510 + .toolbar {
1511 + display: flex;
1512 + align-items: center;
1513 + gap: 0.75rem;
1514 + padding: 0.75rem 2rem;
1459 background: var(--theme-surface); 1515 background: var(--theme-surface);
1516 + border-bottom: 1px solid var(--theme-borde);
1517 + flex-wrap: wrap;
1518 + }
1519 +
1520 + .entity-btn {
1521 + display: flex;
1522 + align-items: center;
1523 + gap: 0.5rem;
1524 + padding: 0.5rem 0.75rem;
1525 + background: var(--theme-body);
1460 border: 1px solid var(--theme-borde); 1526 border: 1px solid var(--theme-borde);
1461 border-radius: 8px; 1527 border-radius: 8px;
1462 - color: var(--theme-texto); 1528 + font-size: 0.8125rem;
1463 - font-size: 0.75rem; 1529 + color: var(--theme-titulo);
1464 cursor: pointer; 1530 cursor: pointer;
1465 - z-index: 60; 1531 + transition: all 0.2s;
1466 - transition: all 0.2s, left 0.3s ease; 1532 + max-width: 280px;
1467 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1468 } 1533 }
1469 1534
1470 - .split-active .split-toggle { 1535 + .entity-btn:hover {
1471 - /* Se ajusta automáticamente con margin-left del contenedor */ 1536 + border-color: var(--theme-accent);
1472 } 1537 }
1473 1538
1474 - @media (min-width: 1024px) { 1539 + .entity-btn .entity-label {
1475 - .split-toggle { 1540 + flex: 1;
1476 - display: flex; 1541 + overflow: hidden;
1542 + text-overflow: ellipsis;
1543 + white-space: nowrap;
1477 } 1544 }
1545 +
1546 + .entity-dropdown {
1547 + position: absolute;
1548 + top: calc(100% + 4px);
1549 + left: 0;
1550 + min-width: 300px;
1551 + background: var(--theme-surface);
1552 + border: 1px solid var(--theme-borde);
1553 + border-radius: 8px;
1554 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
1555 + z-index: 200;
1478 } 1556 }
1479 1557
1480 - .split-toggle:hover { 1558 + .entity-dropdown .dropdown-search {
1481 - background: var(--theme-fill); 1559 + padding: 0.5rem;
1560 + border-bottom: 1px solid var(--theme-borde);
1561 + }
1562 +
1563 + .entity-dropdown .dropdown-search input {
1564 + width: 100%;
1565 + padding: 0.5rem 0.75rem;
1566 + background: var(--theme-body);
1567 + border: 1px solid var(--theme-borde);
1568 + border-radius: 6px;
1569 + font-size: 0.8125rem;
1482 color: var(--theme-titulo); 1570 color: var(--theme-titulo);
1483 - border-color: var(--theme-accent); 1571 + outline: none;
1484 } 1572 }
1485 1573
1486 - .split-toggle.active { 1574 + .entity-dropdown .dropdown-search input:focus {
1487 - background: var(--theme-accent);
1488 - color: white;
1489 border-color: var(--theme-accent); 1575 border-color: var(--theme-accent);
1490 } 1576 }
1491 1577
1492 - .split-toggle-label { 1578 + .entity-dropdown .dropdown-item {
1493 - font-weight: 500; 1579 + display: flex;
1580 + align-items: center;
1581 + gap: 0.5rem;
1582 + width: 100%;
1583 + padding: 0.625rem 0.75rem;
1584 + background: transparent;
1585 + border: none;
1586 + text-align: left;
1587 + font-size: 0.8125rem;
1588 + color: var(--theme-titulo);
1589 + cursor: pointer;
1590 + transition: background 0.15s;
1494 } 1591 }
1495 1592
1496 - /* Info panel */ 1593 + .entity-dropdown .dropdown-item:hover,
1497 - .info-panel { 1594 + .entity-dropdown .dropdown-item.active {
1498 - background: var(--theme-fill); 1595 + background: rgba(201, 167, 81, 0.1);
1499 - border-bottom: 1px solid var(--theme-borde);
1500 - padding: 1.5rem 2rem;
1501 } 1596 }
1502 1597
1503 - .info-content h4 { 1598 + .entity-dropdown .dropdown-item .item-code {
1504 font-family: 'DM Mono', monospace; 1599 font-family: 'DM Mono', monospace;
1505 font-size: 0.625rem; 1600 font-size: 0.625rem;
1506 - letter-spacing: 0.15em;
1507 - text-transform: uppercase;
1508 color: var(--theme-texto); 1601 color: var(--theme-texto);
1509 - margin: 0 0 0.5rem 0; 1602 + flex-shrink: 0;
1510 } 1603 }
1511 1604
1512 - .info-content p { 1605 + .entity-dropdown .dropdown-item .item-name {
1513 - font-size: 0.875rem; 1606 + flex: 1;
1514 - line-height: 1.6; 1607 + overflow: hidden;
1608 + text-overflow: ellipsis;
1609 + white-space: nowrap;
1610 + }
1611 +
1612 + .entity-dropdown .dropdown-item .item-count {
1613 + font-size: 0.6875rem;
1515 color: var(--theme-texto); 1614 color: var(--theme-texto);
1516 - margin: 0 0 1.25rem 0; 1615 + opacity: 0.6;
1517 } 1616 }
1518 1617
1519 - .variacion { 1618 + .entity-dropdown .dropdown-list {
1520 - padding-left: 1rem; 1619 + max-height: 240px;
1521 - border-left: 2px solid var(--theme-borde); 1620 + overflow-y: auto;
1621 + }
1622 +
1623 + /* ═══════════════════════════════════════════════════════════════
1624 + SIDEBAR
1625 + ═══════════════════════════════════════════════════════════════ */
1626 + /* Tree card */
1627 + .tree-card {
1628 + background: var(--theme-surface);
1629 + border-radius: 16px;
1630 + padding: 1.25rem 1.5rem;
1522 margin-bottom: 1rem; 1631 margin-bottom: 1rem;
1523 } 1632 }
1524 1633
1525 - .variacion-rango { 1634 + .tree-card-header {
1526 font-family: 'DM Mono', monospace; 1635 font-family: 'DM Mono', monospace;
1527 - font-size: 0.688rem; 1636 + font-size: 0.625rem;
1637 + letter-spacing: 0.12em;
1528 color: var(--theme-texto); 1638 color: var(--theme-texto);
1639 + margin-bottom: 1rem;
1640 + text-transform: uppercase;
1529 } 1641 }
1530 1642
1531 - .variacion p { 1643 + /* Tree hierarchy */
1532 - margin-top: 0.25rem; 1644 + .tree {
1533 - font-size: 0.813rem;
1534 - }
1535 -
1536 - /* Hijos list in info panel */
1537 - .hijos-list {
1538 display: flex; 1645 display: flex;
1539 flex-direction: column; 1646 flex-direction: column;
1540 - gap: 0.25rem; 1647 + gap: 0.125rem;
1541 - margin-bottom: 1rem;
1542 } 1648 }
1543 1649
1544 - .hijo-link { 1650 + .tree-node {
1545 display: flex; 1651 display: flex;
1546 - align-items: baseline; 1652 + align-items: center;
1547 gap: 0.75rem; 1653 gap: 0.75rem;
1548 - padding: 0.375rem 0.5rem; 1654 + padding: 0.5rem 0.75rem;
1549 - border-radius: 4px; 1655 + margin-left: calc(var(--indent, 0) * 1.5rem);
1656 + border-left: 2px solid #e8f4fa;
1550 text-decoration: none; 1657 text-decoration: none;
1551 - transition: background 0.2s; 1658 + border-radius: 0 8px 8px 0;
1659 + transition: background 0.2s, border-color 0.2s;
1552 } 1660 }
1553 1661
1554 - .hijo-link:hover { 1662 + .tree-node:hover {
1555 - background: var(--theme-fill); 1663 + background: rgba(214, 234, 245, 0.4);
1664 + }
1665 +
1666 + .tree-node-current {
1667 + background: #ddf0f8;
1668 + border-left-color: #ddf0f8;
1669 + border-radius: 8px;
1670 + margin-left: calc(var(--indent, 0) * 1.5rem);
1671 + }
1672 +
1673 + .tree-node-current:hover {
1674 + background: #ddf0f8;
1675 + }
1676 +
1677 + .tree-children {
1678 + display: flex;
1679 + flex-direction: column;
1680 + gap: 0.125rem;
1681 + margin-left: calc(var(--indent, 0) * 1.5rem);
1682 + border-left: 2px solid #f0f7fb;
1683 + padding-left: 0;
1684 + }
1685 +
1686 + .tree-node-child {
1687 + margin-left: 0;
1688 + border-left: none;
1689 + padding-left: 1rem;
1690 + }
1691 +
1692 + .tree-node-child:hover {
1693 + background: rgba(214, 234, 245, 0.4);
1556 } 1694 }
1557 1695
1558 - .hijo-codigo { 1696 + .tree-dot {
1697 + width: 6px;
1698 + height: 6px;
1699 + border-radius: 50%;
1700 + background: #ddf0f8;
1701 + flex-shrink: 0;
1702 + }
1703 +
1704 + .tree-node-current .tree-dot {
1705 + background: var(--theme-titulo);
1706 + }
1707 +
1708 + .tree-code {
1559 font-family: 'DM Mono', monospace; 1709 font-family: 'DM Mono', monospace;
1560 - font-size: 0.688rem; 1710 + font-size: 0.75rem;
1561 color: var(--theme-texto); 1711 color: var(--theme-texto);
1562 min-width: 3rem; 1712 min-width: 3rem;
1563 } 1713 }
1564 1714
1565 - .hijo-nombre { 1715 + .tree-node-current .tree-code {
1566 - font-size: 0.813rem;
1567 color: var(--theme-texto); 1716 color: var(--theme-texto);
1568 } 1717 }
1569 1718
1570 - .hijo-link:hover .hijo-nombre { 1719 + .tree-name {
1720 + font-size: 0.8125rem;
1721 + color: var(--theme-titulo);
1722 + flex: 1;
1723 + }
1724 +
1725 + .tree-node-current .tree-name {
1571 color: var(--theme-titulo); 1726 color: var(--theme-titulo);
1727 + font-weight: 600;
1728 + }
1729 +
1730 + .tree-badge {
1731 + font-family: 'DM Mono', monospace;
1732 + font-size: 0.5625rem;
1733 + text-transform: uppercase;
1734 + letter-spacing: 0.05em;
1735 + color: var(--theme-texto);
1736 + opacity: 0.5;
1737 + padding: 0.125rem 0.375rem;
1738 + background: var(--theme-borde);
1739 + border-radius: 4px;
1740 + }
1741 +
1742 + .tree-node-current .tree-badge {
1743 + background: rgba(255, 255, 255, 0.2);
1744 + color: white;
1745 + opacity: 1;
1746 + }
1747 +
1748 + /* Custom Tooltip */
1749 + .has-tooltip {
1750 + position: relative;
1751 + }
1752 +
1753 + .has-tooltip::after {
1754 + content: attr(data-tooltip);
1755 + position: absolute;
1756 + left: 0;
1757 + top: 100%;
1758 + margin-top: 4px;
1759 + padding: 0.5rem 0.75rem;
1760 + background: #1a1a1a;
1761 + color: #ffffff;
1762 + font-size: 0.75rem;
1763 + font-weight: 400;
1764 + line-height: 1.4;
1765 + border-radius: 6px;
1766 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1767 + white-space: normal;
1768 + max-width: 280px;
1769 + width: max-content;
1770 + z-index: 1000;
1771 + opacity: 0;
1772 + visibility: hidden;
1773 + transform: translateY(-4px);
1774 + transition: opacity 0.15s ease, transform 0.15s ease, visibility 0.15s;
1775 + pointer-events: none;
1776 + }
1777 +
1778 + .has-tooltip:hover::after {
1779 + opacity: 1;
1780 + visibility: visible;
1781 + transform: translateY(0);
1782 + }
1783 +
1784 + /* Tooltip arrow */
1785 + .has-tooltip::before {
1786 + content: '';
1787 + position: absolute;
1788 + left: 12px;
1789 + top: 100%;
1790 + border: 5px solid transparent;
1791 + border-bottom-color: #1a1a1a;
1792 + z-index: 1001;
1793 + opacity: 0;
1794 + visibility: hidden;
1795 + transition: opacity 0.15s ease, visibility 0.15s;
1796 + pointer-events: none;
1797 + }
1798 +
1799 + .has-tooltip:hover::before {
1800 + opacity: 1;
1801 + visibility: visible;
1802 + }
1803 +
1804 + /* Hide tooltip on touch devices to avoid sticky tooltips */
1805 + @media (hover: none) {
1806 + .has-tooltip::after,
1807 + .has-tooltip::before {
1808 + display: none;
1809 + }
1810 + }
1811 +
1812 + /* Tree - modo oscuro */
1813 + :global(html.dark) .tree-node {
1814 + border-left-color: var(--theme-accent);
1815 + }
1816 +
1817 + :global(html.dark) .tree-node:hover {
1818 + background: rgba(201, 167, 81, 0.1);
1819 + }
1820 +
1821 + :global(html.dark) .tree-node-current {
1822 + background: var(--theme-accent);
1823 + border-left-color: var(--theme-accent);
1824 + }
1825 +
1826 + :global(html.dark) .tree-children {
1827 + border-left-color: rgba(201, 167, 81, 0.3);
1828 + }
1829 +
1830 + :global(html.dark) .tree-node-child:hover {
1831 + background: rgba(201, 167, 81, 0.1);
1832 + }
1833 +
1834 + :global(html.dark) .tree-dot {
1835 + background: var(--theme-accent);
1836 + }
1837 +
1838 + /* Vigencia temporal card */
1839 + .vigencia-card {
1840 + background: var(--theme-surface);
1841 + border-radius: 16px;
1842 + padding: 1rem 1.25rem;
1843 + margin-bottom: 1rem;
1844 + display: flex;
1845 + flex-direction: column;
1846 + gap: 0.75rem;
1847 + }
1848 +
1849 + .vigencia-row {
1850 + display: flex;
1851 + align-items: center;
1852 + gap: 0.875rem;
1853 + padding: 0.5rem 0;
1854 + }
1855 +
1856 + .vigencia-row:not(:last-child) {
1857 + border-bottom: 1px solid var(--theme-borde);
1858 + padding-bottom: 0.875rem;
1859 + }
1860 +
1861 + .vigencia-icon {
1862 + color: var(--theme-texto);
1863 + opacity: 0.5;
1864 + flex-shrink: 0;
1865 + }
1866 +
1867 + .vigencia-content {
1868 + flex: 1;
1869 + display: flex;
1870 + flex-direction: column;
1871 + gap: 0.125rem;
1872 + }
1873 +
1874 + .vigencia-status {
1875 + font-size: 0.875rem;
1876 + font-weight: 500;
1877 + color: var(--theme-titulo);
1878 + }
1879 +
1880 + .vigencia-full {
1881 + color: #4A9E6B;
1882 + }
1883 +
1884 + .vigencia-partial {
1885 + color: #D4893D;
1886 + }
1887 +
1888 + .vigencia-changes {
1889 + color: #6B9FD4;
1890 + }
1891 +
1892 + .vigencia-detail {
1893 + font-size: 0.75rem;
1894 + color: var(--theme-texto);
1895 + opacity: 0.7;
1896 + }
1897 +
1898 + .vigencia-btn {
1899 + font-size: 0.75rem;
1900 + font-weight: 500;
1901 + color: #6B9FD4;
1902 + background: rgba(107, 159, 212, 0.1);
1903 + border: none;
1904 + padding: 0.375rem 0.75rem;
1905 + border-radius: 6px;
1906 + cursor: pointer;
1907 + transition: background 0.2s;
1908 + white-space: nowrap;
1909 + }
1910 +
1911 + .vigencia-btn:hover {
1912 + background: rgba(107, 159, 212, 0.2);
1913 + }
1914 +
1915 + /* Modal */
1916 + .modal-overlay {
1917 + position: fixed;
1918 + inset: 0;
1919 + background: rgba(0, 0, 0, 0.6);
1920 + backdrop-filter: blur(4px);
1921 + display: flex;
1922 + align-items: center;
1923 + justify-content: center;
1924 + z-index: 1000;
1925 + padding: 1rem;
1926 + }
1927 +
1928 + .modal-content {
1929 + background: var(--theme-surface);
1930 + border-radius: 16px;
1931 + max-width: 560px;
1932 + width: 100%;
1933 + max-height: 80vh;
1934 + overflow: hidden;
1935 + display: flex;
1936 + flex-direction: column;
1937 + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2);
1938 + }
1939 +
1940 + .modal-header {
1941 + display: flex;
1942 + align-items: center;
1943 + justify-content: space-between;
1944 + padding: 1.25rem 1.5rem;
1945 + border-bottom: 1px solid var(--theme-borde);
1946 + }
1947 +
1948 + .modal-header h3 {
1949 + font-size: 1rem;
1950 + font-weight: 600;
1951 + color: var(--theme-titulo);
1952 + margin: 0;
1953 + }
1954 +
1955 + .modal-close {
1956 + color: var(--theme-texto);
1957 + background: transparent;
1958 + border: none;
1959 + padding: 0.25rem;
1960 + cursor: pointer;
1961 + border-radius: 6px;
1962 + transition: background 0.2s;
1963 + }
1964 +
1965 + .modal-close:hover {
1966 + background: var(--theme-borde);
1967 + }
1968 +
1969 + .modal-body {
1970 + padding: 1.5rem;
1971 + overflow-y: auto;
1972 + display: flex;
1973 + flex-direction: column;
1974 + gap: 1rem;
1975 + }
1976 +
1977 + .def-item {
1978 + padding: 1rem;
1979 + background: var(--theme-body);
1980 + border-radius: 10px;
1981 + border-left: 3px solid var(--theme-borde);
1982 + }
1983 +
1984 + .def-current {
1985 + border-left-color: #6B9FD4;
1986 + }
1987 +
1988 + .def-header {
1989 + display: flex;
1990 + align-items: center;
1991 + gap: 0.5rem;
1992 + margin-bottom: 0.5rem;
1993 + }
1994 +
1995 + .def-period {
1996 + font-family: 'DM Mono', monospace;
1997 + font-size: 0.75rem;
1998 + font-weight: 500;
1999 + color: #6B9FD4;
2000 + }
2001 +
2002 + .def-badge {
2003 + font-size: 0.625rem;
2004 + font-weight: 600;
2005 + text-transform: uppercase;
2006 + letter-spacing: 0.05em;
2007 + color: white;
2008 + background: #4A9E6B;
2009 + padding: 0.125rem 0.5rem;
2010 + border-radius: 4px;
2011 + }
2012 +
2013 + .def-text {
2014 + font-size: 0.875rem;
2015 + color: var(--theme-texto);
2016 + line-height: 1.6;
2017 + margin: 0;
2018 + }
2019 +
2020 + /* Modal - modo oscuro */
2021 + :global(html.dark) .vigencia-btn {
2022 + color: var(--theme-accent);
2023 + background: rgba(201, 167, 81, 0.1);
2024 + }
2025 +
2026 + :global(html.dark) .vigencia-btn:hover {
2027 + background: rgba(201, 167, 81, 0.2);
2028 + }
2029 +
2030 + :global(html.dark) .def-current {
2031 + border-left-color: var(--theme-accent);
2032 + }
2033 +
2034 + :global(html.dark) .def-period {
2035 + color: var(--theme-accent);
1572 } 2036 }
1573 2037
1574 /* Main */ 2038 /* Main */
...@@ -1610,51 +2074,7 @@ ...@@ -1610,51 +2074,7 @@
1610 } 2074 }
1611 } 2075 }
1612 2076
1613 - .selector-label { 2077 + /* Old dropdown styles - keeping for comparar view */
1614 - font-size: 0.813rem;
1615 - color: var(--theme-texto);
1616 - }
1617 -
1618 - .selector-btn {
1619 - display: flex;
1620 - align-items: center;
1621 - gap: 0.5rem;
1622 - padding: 0.5rem 1rem;
1623 - background: transparent;
1624 - border: none;
1625 - box-shadow: inset 0 0 0 1px var(--theme-borde);
1626 - border-radius: 8px;
1627 - color: var(--theme-titulo);
1628 - font-size: 0.875rem;
1629 - cursor: pointer;
1630 - transition: all 0.2s;
1631 - }
1632 - .selector-btn:hover {
1633 - box-shadow: inset 0 0 0 1px var(--theme-accent);
1634 - }
1635 - .selector-btn svg {
1636 - color: var(--theme-texto);
1637 - }
1638 -
1639 - .relative {
1640 - position: relative;
1641 - }
1642 -
1643 - .dropdown-panel {
1644 - position: absolute;
1645 - top: 100%;
1646 - left: 0;
1647 - margin-top: 0.25rem;
1648 - width: min(500px, 90vw);
1649 - max-height: 360px;
1650 - overflow: hidden;
1651 - background: var(--theme-surface);
1652 - border: 1px solid var(--theme-borde);
1653 - border-radius: 8px;
1654 - box-shadow: 0 10px 40px rgba(0,0,0,0.4);
1655 - z-index: 50;
1656 - }
1657 -
1658 .dropdown-search { 2078 .dropdown-search {
1659 padding: 0.5rem; 2079 padding: 0.5rem;
1660 border-bottom: 1px solid var(--theme-borde); 2080 border-bottom: 1px solid var(--theme-borde);
...@@ -1721,53 +2141,120 @@ ...@@ -1721,53 +2141,120 @@
1721 margin-bottom: 1rem; 2141 margin-bottom: 1rem;
1722 } 2142 }
1723 2143
1724 - .chart-protagonista { 2144 + .chart-protagonista {
1725 - margin-bottom: 1.25rem; 2145 + background: var(--theme-surface);
2146 + border-radius: 16px;
2147 + padding: 1.25rem 1.5rem;
2148 + display: flex;
2149 + flex-direction: column;
2150 + min-width: 0;
2151 + width: 100%;
2152 + transition: flex 0.3s ease;
2153 + }
2154 +
2155 + .chart-protagonista.expanded {
2156 + flex: 1;
2157 + }
2158 +
2159 + .chart-protagonista .chart-wrapper {
2160 + flex: 1;
2161 + display: flex;
2162 + align-items: stretch;
2163 + min-height: 220px;
2164 + transition: min-height 0.3s ease;
2165 + }
2166 +
2167 + .chart-protagonista.expanded .chart-wrapper {
2168 + min-height: 320px;
2169 + }
2170 +
2171 + .chart-protagonista .chart-container {
2172 + height: 100%;
2173 + }
2174 +
2175 + .chart-protagonista .y-axis {
2176 + padding-bottom: 1.5rem;
2177 + }
2178 +
2179 + .chart-section:has(.chart-comparar) {
2180 + margin-bottom: 0;
2181 + }
2182 +
2183 + .chart-section:has(.chart-comparar) .y-axis {
2184 + height: calc(100vh - 380px);
2185 + min-height: 140px;
2186 + max-height: 280px;
2187 + }
2188 +
2189 + .chart-top-row {
2190 + display: flex;
2191 + justify-content: space-between;
2192 + align-items: center;
2193 + margin-bottom: 0.375rem;
2194 + }
2195 +
2196 + .main-title-pills {
2197 + display: flex;
2198 + gap: 0.5rem;
1726 } 2199 }
1727 2200
1728 - .chart-protagonista .chart { 2201 + .pill {
1729 - height: calc(100vh - 480px); 2202 + font-family: 'DM Mono', monospace;
1730 - min-height: 120px; 2203 + font-size: 0.75rem;
1731 - max-height: 200px; 2204 + font-weight: 500;
2205 + padding: 0.375rem 0.75rem;
2206 + border-radius: 6px;
2207 + letter-spacing: 0.03em;
1732 } 2208 }
1733 2209
1734 - .chart-protagonista .y-axis { 2210 + .pill-nivel {
1735 - height: calc(100vh - 480px); 2211 + background: rgba(166, 138, 62, 0.12);
1736 - min-height: 120px; 2212 + color: #C9A751;
1737 - max-height: 200px;
1738 } 2213 }
1739 2214
1740 - .chart-section:has(.chart-comparar) { 2215 + .pill-codigo {
1741 - margin-bottom: 0; 2216 + background: rgba(107, 159, 212, 0.12);
2217 + color: #6B9FD4;
1742 } 2218 }
1743 2219
1744 - .chart-section:has(.chart-comparar) .y-axis { 2220 + .main-title-name {
1745 - height: calc(100vh - 380px); 2221 + font-size: 1.375rem;
1746 - min-height: 140px; 2222 + font-weight: 600;
1747 - max-height: 280px; 2223 + color: var(--theme-titulo);
2224 + margin: 0 0 0.5rem 0;
2225 + line-height: 1.2;
1748 } 2226 }
1749 2227
1750 .chart-header { 2228 .chart-header {
1751 - display: flex;
1752 - justify-content: space-between;
1753 - align-items: baseline;
1754 margin-bottom: 0.5rem; 2229 margin-bottom: 0.5rem;
1755 } 2230 }
1756 2231
1757 .chart-title { 2232 .chart-title {
2233 + font-family: 'Qanelas', var(--font-sans);
1758 font-size: 0.75rem; 2234 font-size: 0.75rem;
2235 + font-weight: 500;
1759 color: var(--theme-texto); 2236 color: var(--theme-texto);
1760 text-transform: uppercase; 2237 text-transform: uppercase;
1761 - letter-spacing: 0.1em; 2238 + letter-spacing: 0.08em;
1762 } 2239 }
1763 2240
1764 .chart-period { 2241 .chart-period {
1765 - font-family: 'DM Mono', monospace; 2242 + font-family: 'Qanelas', var(--font-sans);
1766 - font-size: 0.875rem; 2243 + font-size: 0.75rem;
2244 + font-weight: 400;
1767 color: var(--theme-texto); 2245 color: var(--theme-texto);
1768 - font-variant-numeric: tabular-nums; 2246 + opacity: 0.7;
1769 - min-width: 5rem; 2247 + text-transform: none;
1770 - text-align: right; 2248 + letter-spacing: 0;
2249 + }
2250 +
2251 + .chart-controls {
2252 + display: flex;
2253 + align-items: center;
2254 + justify-content: space-between;
2255 + gap: 1rem;
2256 + margin-bottom: 0.75rem;
2257 + min-width: 0;
1771 } 2258 }
1772 2259
1773 .chart-wrapper { 2260 .chart-wrapper {
...@@ -1817,24 +2304,46 @@ ...@@ -1817,24 +2304,46 @@
1817 flex-direction: column; 2304 flex-direction: column;
1818 justify-content: space-between; 2305 justify-content: space-between;
1819 align-items: flex-end; 2306 align-items: flex-end;
1820 - height: 160px; 2307 + padding-right: 0.5rem;
1821 - padding-bottom: 1.5rem; 2308 + width: 4rem;
2309 + flex-shrink: 0;
1822 } 2310 }
1823 2311
1824 .y-label { 2312 .y-label {
1825 - font-family: 'DM Mono', monospace; 2313 + font-family: 'Qanelas', var(--font-sans);
1826 - font-size: 0.625rem; 2314 + font-size: 0.6875rem;
1827 color: var(--theme-texto); 2315 color: var(--theme-texto);
2316 + font-weight: 400;
2317 + line-height: 1;
2318 + white-space: nowrap;
1828 } 2319 }
1829 2320
1830 - .chart { 2321 + .chart-container {
1831 flex: 1; 2322 flex: 1;
2323 + position: relative;
2324 + min-height: 160px;
2325 + overflow: hidden;
2326 + }
2327 +
2328 + .grid-line {
2329 + position: absolute;
2330 + left: 0;
2331 + right: 0;
2332 + border-top: 1px dashed var(--theme-texto);
2333 + opacity: 0.3;
2334 + pointer-events: none;
2335 + }
2336 +
2337 + .chart {
2338 + position: absolute;
2339 + top: 0;
2340 + left: 0;
2341 + right: 0;
2342 + bottom: 1.5rem;
1832 display: flex; 2343 display: flex;
1833 align-items: flex-end; 2344 align-items: flex-end;
1834 gap: 4px; 2345 gap: 4px;
1835 - height: 160px;
1836 padding: 0 0.5rem; 2346 padding: 0 0.5rem;
1837 - border-left: 1px solid var(--theme-borde);
1838 } 2347 }
1839 2348
1840 .bar-container { 2349 .bar-container {
...@@ -1849,20 +2358,19 @@ ...@@ -1849,20 +2358,19 @@
1849 2358
1850 .bar { 2359 .bar {
1851 width: 100%; 2360 width: 100%;
1852 - background: #2a9d8f; 2361 + background: #555555;
1853 border-radius: 4px 4px 0 0; 2362 border-radius: 4px 4px 0 0;
1854 transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); 2363 transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
1855 - min-height: 4px; 2364 + min-height: 6px;
1856 } 2365 }
1857 2366
1858 .bar.hovered { 2367 .bar.hovered {
1859 background: var(--theme-accent); 2368 background: var(--theme-accent);
1860 - transform: scaleX(1.1);
1861 } 2369 }
1862 2370
1863 .bar-label { 2371 .bar-label {
1864 - font-family: 'DM Mono', monospace; 2372 + font-family: 'Qanelas', var(--font-sans);
1865 - font-size: 0.625rem; 2373 + font-size: 0.6875rem;
1866 color: var(--theme-texto); 2374 color: var(--theme-texto);
1867 margin-top: 0.5rem; 2375 margin-top: 0.5rem;
1868 opacity: 0; 2376 opacity: 0;
...@@ -1875,133 +2383,128 @@ ...@@ -1875,133 +2383,128 @@
1875 display: grid; 2383 display: grid;
1876 grid-template-columns: repeat(4, 1fr); 2384 grid-template-columns: repeat(4, 1fr);
1877 gap: 0.5rem; 2385 gap: 0.5rem;
1878 - margin-bottom: 1rem; 2386 + margin-top: 1rem;
1879 } 2387 }
1880 2388
1881 .kpi { 2389 .kpi {
1882 - background: transparent;
1883 - box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
1884 - padding: 0.75rem 0.75rem;
1885 text-align: center; 2390 text-align: center;
2391 + min-width: 0;
2392 + padding: 0.625rem 0.5rem;
2393 + background: var(--theme-body);
1886 border-radius: 8px; 2394 border-radius: 8px;
1887 - transition: box-shadow 0.3s ease; 2395 + border: 1px solid var(--theme-borde);
1888 } 2396 }
1889 2397
1890 .kpi-value { 2398 .kpi-value {
1891 display: block; 2399 display: block;
1892 - font-family: 'DM Mono', monospace; 2400 + font-family: 'Qanelas', var(--font-sans);
1893 - font-size: 1.125rem; 2401 + font-size: 0.9375rem;
1894 - font-weight: 500; 2402 + font-weight: 600;
1895 - color: var(--theme-titulo); 2403 + color: #666666;
1896 - margin-bottom: 0; 2404 + margin-bottom: 0.125rem;
1897 transition: opacity 0.2s ease; 2405 transition: opacity 0.2s ease;
1898 font-variant-numeric: tabular-nums; 2406 font-variant-numeric: tabular-nums;
1899 - min-height: 1.5rem; 2407 + min-height: auto;
2408 + white-space: nowrap;
2409 + overflow: hidden;
2410 + text-overflow: ellipsis;
1900 } 2411 }
1901 2412
1902 .kpi-label { 2413 .kpi-label {
1903 font-size: 0.5625rem; 2414 font-size: 0.5625rem;
1904 - color: var(--theme-texto); 2415 + font-weight: 600;
2416 + color: #666666;
1905 text-transform: uppercase; 2417 text-transform: uppercase;
1906 letter-spacing: 0.03em; 2418 letter-spacing: 0.03em;
1907 line-height: 1.3; 2419 line-height: 1.3;
1908 transition: opacity 0.2s ease; 2420 transition: opacity 0.2s ease;
1909 - min-height: 1.25rem; 2421 + min-height: auto;
1910 display: flex; 2422 display: flex;
1911 align-items: center; 2423 align-items: center;
1912 justify-content: center; 2424 justify-content: center;
1913 } 2425 }
1914 2426
1915 - /* Top entidades */ 2427 + /* Ranking Simple */
1916 - .top-header { 2428 + .ranking-section {
1917 - display: flex; 2429 + background: var(--theme-surface);
1918 - justify-content: space-between; 2430 + border-radius: 16px;
1919 - align-items: baseline; 2431 + padding: 1.25rem 1.5rem;
1920 - margin-bottom: 0.625rem;
1921 } 2432 }
1922 2433
1923 - .top-section h3 { 2434 + .ranking-title {
1924 - font-family: 'DM Mono', monospace; 2435 + font-size: 1.375rem;
1925 - font-size: 0.75rem; 2436 + font-weight: 600;
1926 - color: var(--theme-texto); 2437 + color: var(--theme-titulo);
1927 - text-transform: uppercase; 2438 + margin: 0 0 0.5rem 0;
1928 - letter-spacing: 0.1em; 2439 + line-height: 1.2;
1929 - margin: 0;
1930 } 2440 }
1931 2441
1932 - .top-context { 2442 + .ranking-desc {
1933 - font-family: 'DM Mono', monospace; 2443 + font-size: 0.8125rem;
1934 - font-size: 0.688rem; 2444 + color: #666666;
1935 - color: var(--theme-texto); 2445 + margin: 0 0 1.25rem 0;
1936 - font-variant-numeric: tabular-nums;
1937 } 2446 }
1938 2447
1939 - .top-list { 2448 + .ranking-cards {
1940 display: flex; 2449 display: flex;
1941 flex-direction: column; 2450 flex-direction: column;
1942 - gap: 0.5rem; 2451 + gap: 0.25rem;
1943 } 2452 }
1944 2453
1945 - .top-item { 2454 + .ranking-card {
1946 display: flex; 2455 display: flex;
1947 align-items: center; 2456 align-items: center;
1948 gap: 0.75rem; 2457 gap: 0.75rem;
1949 - padding: 0.625rem 0.875rem; 2458 + padding: 0.25rem 0;
1950 - background: transparent; 2459 + min-width: 0;
1951 - box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
1952 - border-radius: 8px;
1953 - transition: box-shadow 0.25s ease;
1954 } 2460 }
1955 2461
1956 - .top-item:hover { 2462 + .ranking-position {
1957 - box-shadow: inset 0 0 0 1px rgba(201, 167, 81, 0.4); 2463 + font-family: 'Qanelas', var(--font-sans);
2464 + font-size: 1rem;
2465 + font-weight: 600;
2466 + color: #555555;
2467 + flex-shrink: 0;
1958 } 2468 }
1959 2469
1960 - .top-rank { 2470 + .ranking-list {
1961 - font-family: 'DM Mono', monospace; 2471 + display: flex;
1962 - font-size: 0.875rem; 2472 + flex-direction: column;
1963 - color: var(--theme-texto); 2473 + gap: 0.75rem;
1964 - width: 24px;
1965 } 2474 }
1966 2475
1967 - .top-info { 2476 + .ranking-item {
1968 - flex: 1; 2477 + display: flex;
2478 + align-items: center;
2479 + gap: 0.75rem;
2480 + padding: 0.5rem 0;
2481 + border-bottom: 1px solid var(--theme-borde);
2482 + min-width: 0;
1969 } 2483 }
1970 2484
1971 - .top-name { 2485 + .ranking-item:last-child {
1972 - display: block; 2486 + border-bottom: none;
1973 - font-size: 0.8125rem;
1974 - color: var(--theme-titulo);
1975 - margin-bottom: 0.375rem;
1976 } 2487 }
1977 2488
1978 - .top-bar-bg { 2489 + .ranking-name {
1979 - height: 4px; 2490 + flex: 1;
1980 - background: var(--theme-surface); 2491 + font-size: 0.8125rem;
1981 - border-radius: 2px; 2492 + font-weight: 400;
2493 + color: #666666;
2494 + line-height: 1.3;
2495 + white-space: nowrap;
1982 overflow: hidden; 2496 overflow: hidden;
2497 + text-overflow: ellipsis;
1983 } 2498 }
1984 2499
1985 - .top-bar { 2500 + .ranking-pct {
1986 - height: 100%; 2501 + font-family: 'Qanelas', var(--font-sans);
1987 - background: var(--theme-accent); 2502 + font-size: 1rem;
1988 - border-radius: 2px; 2503 + font-weight: 600;
1989 - transition: width 0.35s cubic-bezier(0.4, 0, 0.2, 1), background 0.25s ease; 2504 + color: #555555;
1990 - } 2505 + flex-shrink: 0;
1991 - 2506 + min-width: 3.5rem;
1992 - .top-item:hover .top-bar {
1993 - background: var(--theme-accent);
1994 - }
1995 -
1996 - .top-pct {
1997 - font-family: 'DM Mono', monospace;
1998 - font-size: 0.875rem;
1999 - color: var(--theme-texto);
2000 - min-width: 52px;
2001 - width: 52px;
2002 text-align: right; 2507 text-align: right;
2003 - transition: opacity 0.2s ease;
2004 - font-variant-numeric: tabular-nums;
2005 } 2508 }
2006 2509
2007 .see-all { 2510 .see-all {
...@@ -2021,38 +2524,7 @@ ...@@ -2021,38 +2524,7 @@
2021 color: var(--theme-accent); 2524 color: var(--theme-accent);
2022 } 2525 }
2023 2526
2024 - /* Vista Toggle */ 2527 + /* Comparador inline */
2025 - .vista-toggle {
2026 - display: inline-flex;
2027 - background: transparent;
2028 - box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
2029 - border-radius: 10px;
2030 - padding: 4px;
2031 - margin-bottom: 0.75rem;
2032 - }
2033 -
2034 - .toggle-btn {
2035 - padding: 0.5rem 1.25rem;
2036 - font-size: 0.813rem;
2037 - font-weight: 500;
2038 - color: var(--theme-texto);
2039 - background: transparent;
2040 - border: none;
2041 - border-radius: 7px;
2042 - cursor: pointer;
2043 - transition: all 0.2s;
2044 - }
2045 -
2046 - .toggle-btn:hover {
2047 - color: var(--theme-texto);
2048 - }
2049 -
2050 - .toggle-btn.active {
2051 - background: var(--theme-titulo);
2052 - color: var(--theme-body);
2053 - }
2054 -
2055 - /* Comparador inline - ajustado para 100vh */
2056 .comparador-selectors { 2528 .comparador-selectors {
2057 display: flex; 2529 display: flex;
2058 align-items: flex-start; 2530 align-items: flex-start;
...@@ -2333,22 +2805,120 @@ ...@@ -2333,22 +2805,120 @@
2333 } 2805 }
2334 2806
2335 /* Responsive */ 2807 /* Responsive */
2808 + @media (max-width: 900px) {
2809 + .dashboard-layout {
2810 + grid-template-columns: 1fr;
2811 + padding: 1rem;
2812 + }
2813 +
2814 + .dashboard-context {
2815 + height: auto;
2816 + }
2817 +
2818 + /* Mobile sidebar toggle */
2819 + .mobile-sidebar-toggle {
2820 + display: flex;
2821 + align-items: center;
2822 + gap: 0.5rem;
2823 + padding: 0.625rem 1rem;
2824 + background: var(--theme-surface);
2825 + border: 1px solid var(--theme-borde);
2826 + border-radius: 10px;
2827 + color: var(--theme-titulo);
2828 + font-family: 'Qanelas', var(--font-sans);
2829 + font-size: 0.875rem;
2830 + font-weight: 500;
2831 + cursor: pointer;
2832 + transition: all 0.2s ease;
2833 + margin-bottom: 0.5rem;
2834 + }
2835 +
2836 + .mobile-sidebar-toggle:hover {
2837 + background: var(--theme-surface-hover);
2838 + }
2839 +
2840 + .mobile-sidebar-toggle svg {
2841 + color: var(--theme-texto);
2842 + opacity: 0.7;
2843 + }
2844 +
2845 + .toggle-label {
2846 + flex: 1;
2847 + text-align: left;
2848 + }
2849 +
2850 + /* Ocultar contenido del sidebar en mobile por defecto */
2851 + .dashboard-context .context-card {
2852 + display: none;
2853 + height: auto;
2854 + }
2855 +
2856 + /* Mostrar cuando está abierto */
2857 + .dashboard-context.mobile-open .context-card {
2858 + display: flex;
2859 + animation: slideDown 0.25s ease;
2860 + }
2861 +
2862 + @keyframes slideDown {
2863 + from {
2864 + opacity: 0;
2865 + transform: translateY(-8px);
2866 + }
2867 + to {
2868 + opacity: 1;
2869 + transform: translateY(0);
2870 + }
2871 + }
2872 +
2873 + .context-card {
2874 + height: auto;
2875 + }
2876 +
2877 + .sidebar-card {
2878 + flex: 1;
2879 + min-width: 200px;
2880 + }
2881 +
2882 + .sidebar-separator {
2883 + display: none;
2884 + }
2885 +
2886 + .sidebar-section {
2887 + width: 100%;
2888 + }
2889 +
2890 + .sidebar-selectors {
2891 + flex-direction: row;
2892 + flex-wrap: wrap;
2893 + }
2894 + }
2895 +
2336 @media (max-width: 640px) { 2896 @media (max-width: 640px) {
2337 - header, main { 2897 + .page-nav {
2338 - padding: 1.25rem 1rem; 2898 + padding: 0.75rem 1rem;
2339 } 2899 }
2340 2900
2341 - .title-main { 2901 + .toolbar {
2902 + padding: 0.75rem 1rem;
2342 flex-direction: column; 2903 flex-direction: column;
2343 - gap: 0.25rem; 2904 + align-items: stretch;
2905 + gap: 0.5rem;
2344 } 2906 }
2345 2907
2346 - .codigo { 2908 + .dashboard-layout {
2347 - font-size: 1.25rem; 2909 + padding: 0.75rem;
2348 } 2910 }
2349 2911
2350 - h1 { 2912 + .context-card {
2351 - font-size: 1.375rem; 2913 + height: auto;
2914 + }
2915 +
2916 + .sidebar-selectors {
2917 + flex-direction: column;
2918 + }
2919 +
2920 + main, .dashboard-main {
2921 + padding: 0;
2352 } 2922 }
2353 2923
2354 .y-axis { 2924 .y-axis {
...@@ -2360,21 +2930,21 @@ ...@@ -2360,21 +2930,21 @@
2360 height: 120px; 2930 height: 120px;
2361 } 2931 }
2362 2932
2933 + .chart-protagonista .chart-container {
2934 + min-height: 160px;
2935 + max-height: 200px;
2936 + }
2937 +
2363 .kpis { 2938 .kpis {
2364 grid-template-columns: repeat(2, 1fr); 2939 grid-template-columns: repeat(2, 1fr);
2365 } 2940 }
2366 2941
2367 .kpi-value { 2942 .kpi-value {
2368 - font-size: 1.125rem; 2943 + font-size: 0.75rem;
2369 - }
2370 -
2371 - .top-summary {
2372 - flex-direction: column;
2373 - gap: 0.25rem;
2374 } 2944 }
2375 2945
2376 - .comparador-modal { 2946 + .kpi-label {
2377 - padding: 1.5rem; 2947 + font-size: 0.45rem;
2378 } 2948 }
2379 2949
2380 .comparador-selectors { 2950 .comparador-selectors {
...@@ -2386,21 +2956,27 @@ ...@@ -2386,21 +2956,27 @@
2386 display: none; 2956 display: none;
2387 } 2957 }
2388 2958
2389 - .kpi-values { 2959 + /* Ranking responsive */
2390 - gap: 1rem; 2960 + .ranking-title {
2961 + font-size: 1.25rem;
2391 } 2962 }
2392 2963
2393 - .kpi-val { 2964 + .ranking-cards {
2394 - font-size: 0.75rem; 2965 + grid-template-columns: 1fr;
2395 - min-width: 60px;
2396 } 2966 }
2397 2967
2398 - .comp-bar { 2968 + .ranking-card {
2399 - width: 12px; 2969 + padding: 0.75rem;
2400 } 2970 }
2401 2971
2402 - .grouped-chart { 2972 + .ranking-name {
2403 - gap: 0.5rem; 2973 + font-size: 0.8125rem;
2974 + -webkit-line-clamp: 1;
2975 + }
2976 +
2977 + .ranking-position,
2978 + .ranking-pct {
2979 + font-size: 0.875rem;
2404 } 2980 }
2405 } 2981 }
2406 2982
......