Rafael Lopez

treemap

...@@ -106,6 +106,43 @@ ...@@ -106,6 +106,43 @@
106 } 106 }
107 107
108 /* ============================================ 108 /* ============================================
109 + SISTEMA DE TEMAS (LIGHT/DARK MODE)
110 + Para cambiar la paleta, solo modifica estos valores
111 + ============================================ */
112 +
113 +:root {
114 + /* Modo claro (default) - Slate/White */
115 + --theme-body: #fafafa;
116 + --theme-background: #f8fafc;
117 + --theme-titulo: #1a1a1a;
118 + --theme-texto: #64748b;
119 + --theme-fill: #f1f5f9;
120 + --theme-borde: rgba(0, 0, 0, 0.08);
121 + --theme-accent: #c9a751;
122 + --theme-accent-hover: #b8963f;
123 +
124 + /* Superficies alternativas */
125 + --theme-surface: #ffffff;
126 + --theme-surface-hover: #f8fafc;
127 +}
128 +
129 +:root.dark {
130 + /* Modo oscuro */
131 + --theme-body: #0d0d0d;
132 + --theme-background: #1a1a1a;
133 + --theme-titulo: #e8e4d8;
134 + --theme-texto: #b8b3a3;
135 + --theme-fill: #2d2d2d;
136 + --theme-borde: rgba(255, 255, 255, 0.1);
137 + --theme-accent: #c9a751;
138 + --theme-accent-hover: #d4b76a;
139 +
140 + /* Superficies alternativas */
141 + --theme-surface: #1a1a1a;
142 + --theme-surface-hover: #2d2d2d;
143 +}
144 +
145 +/* ============================================
109 ESTILOS BASE 146 ESTILOS BASE
110 ============================================ */ 147 ============================================ */
111 148
...@@ -115,22 +152,51 @@ html { ...@@ -115,22 +152,51 @@ html {
115 152
116 body { 153 body {
117 font-family: var(--font-sans); 154 font-family: var(--font-sans);
118 - color: var(--color-graphite); 155 + color: var(--theme-titulo);
119 - background-color: #FAFAFA; 156 + background-color: var(--theme-body);
120 -webkit-font-smoothing: antialiased; 157 -webkit-font-smoothing: antialiased;
121 -moz-osx-font-smoothing: grayscale; 158 -moz-osx-font-smoothing: grayscale;
159 + transition: background-color 0.2s ease, color 0.2s ease;
122 } 160 }
123 161
124 /* Focus ring consistente con identidad */ 162 /* Focus ring consistente con identidad */
125 *:focus-visible { 163 *:focus-visible {
126 - outline: 2px solid var(--color-gold); 164 + outline: 2px solid var(--theme-accent);
127 outline-offset: 2px; 165 outline-offset: 2px;
128 } 166 }
129 167
130 /* Selección de texto */ 168 /* Selección de texto */
131 ::selection { 169 ::selection {
132 - background-color: var(--color-gold); 170 + background-color: var(--theme-accent);
133 - color: white; 171 + color: var(--theme-body);
172 +}
173 +
174 +/* ============================================
175 + UTILIDADES DE TEMA
176 + ============================================ */
177 +
178 +.theme-bg {
179 + background-color: var(--theme-body);
180 +}
181 +
182 +.theme-surface {
183 + background-color: var(--theme-surface);
184 +}
185 +
186 +.theme-text {
187 + color: var(--theme-titulo);
188 +}
189 +
190 +.theme-text-muted {
191 + color: var(--theme-texto);
192 +}
193 +
194 +.theme-border {
195 + border-color: var(--theme-borde);
196 +}
197 +
198 +.theme-accent {
199 + color: var(--theme-accent);
134 } 200 }
135 201
136 /* ============================================ 202 /* ============================================
......
1 <!doctype html> 1 <!doctype html>
2 -<html lang="en"> 2 +<html lang="es">
3 <head> 3 <head>
4 <meta charset="utf-8" /> 4 <meta charset="utf-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1" />
6 +
7 + <!-- Theme initialization - ANTES de cargar CSS para evitar flash -->
8 + <script>
9 + (function() {
10 + const savedTheme = localStorage.getItem('theme');
11 + if (savedTheme === 'dark') {
12 + document.documentElement.classList.add('dark');
13 + }
14 + // Por defecto modo claro (no detectar sistema)
15 + })();
16 + </script>
17 +
6 %sveltekit.head% 18 %sveltekit.head%
7 </head> 19 </head>
8 <body data-sveltekit-preload-data="hover"> 20 <body data-sveltekit-preload-data="hover">
......
1 +<script>
2 + import { page } from '$app/stores';
3 + import { onMount } from 'svelte';
4 + import { drawerOpen, closeDrawer } from '$lib/stores/drawer.js';
5 +
6 + let isDarkMode = $state(false);
7 +
8 + const clasificadores = [
9 + { href: '/clasificadores/institucional', nombre: 'Institucional', desc: 'Quién gasta' },
10 + { href: '/clasificadores/objeto-gasto', nombre: 'Objeto del Gasto', desc: 'En qué' },
11 + { href: '/clasificadores/rubros', nombre: 'Rubros', desc: 'Cuánto ingresa' },
12 + { href: '/clasificadores/funcional', nombre: 'Funcional', desc: 'Para qué' },
13 + { href: '/clasificadores/fuente', nombre: 'Fuente', desc: 'De dónde' },
14 + { href: '/clasificadores/organismo', nombre: 'Organismo', desc: 'Quién financia' },
15 + { href: '/clasificadores/sectores', nombre: 'Sectores', desc: 'Qué sector' },
16 + ];
17 +
18 + function isActive(href) {
19 + return $page.url.pathname.startsWith(href);
20 + }
21 +
22 + function toggleTheme() {
23 + isDarkMode = !isDarkMode;
24 + document.documentElement.classList.toggle('dark', isDarkMode);
25 + localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
26 + }
27 +
28 + onMount(() => {
29 + isDarkMode = document.documentElement.classList.contains('dark');
30 +
31 + const observer = new MutationObserver(() => {
32 + isDarkMode = document.documentElement.classList.contains('dark');
33 + });
34 + observer.observe(document.documentElement, {
35 + attributes: true,
36 + attributeFilter: ['class']
37 + });
38 +
39 + return () => observer.disconnect();
40 + });
41 +</script>
42 +
43 +<!-- Overlay (fondo oscuro) -->
44 +{#if $drawerOpen}
45 + <div
46 + class="fixed inset-0 bg-black/40 z-[200]"
47 + style="opacity: {$drawerOpen ? '1' : '0'}; transition: opacity 0.2s ease-out;"
48 + onclick={closeDrawer}
49 + onkeydown={(e) => e.key === 'Escape' && closeDrawer()}
50 + role="button"
51 + tabindex="0"
52 + aria-label="Cerrar menú"
53 + ></div>
54 +{/if}
55 +
56 +<!-- Drawer -->
57 +<aside
58 + class="fixed top-0 right-0 h-full w-80 border-l shadow-2xl z-[201] flex flex-col"
59 + style="
60 + background-color: var(--theme-body);
61 + border-color: rgba(0, 0, 0, 0.05);
62 + transform: {$drawerOpen ? 'translate3d(0, 0, 0)' : 'translate3d(100%, 0, 0)'};
63 + transition: transform 0.3s cubic-bezier(0.33, 1, 0.68, 1);
64 + "
65 +>
66 + <!-- Header del Drawer -->
67 + <div class="py-3 md:py-4 px-4 md:px-6 border-b flex-shrink-0 relative" style="border-color: rgba(0, 0, 0, 0.05);">
68 +
69 + <!-- Theme Toggle - SOLO en móvil (arriba izquierda del drawer) -->
70 + <div class="absolute top-3 md:top-4 left-4 md:left-6 md:hidden">
71 + <button
72 + onclick={toggleTheme}
73 + class="p-2 rounded-full transition-all"
74 + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;"
75 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
76 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
77 + aria-label={isDarkMode ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
78 + >
79 + {#if isDarkMode}
80 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
81 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
82 + </svg>
83 + {:else}
84 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
85 + <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
86 + </svg>
87 + {/if}
88 + </button>
89 + </div>
90 +
91 + <!-- Botón cerrar (X) - arriba derecha -->
92 + <div class="group absolute top-3 md:top-4 right-4 md:right-6">
93 + <button
94 + onclick={closeDrawer}
95 + class="p-2 rounded-lg transition-colors flex-shrink-0"
96 + style="color: var(--theme-texto); cursor: pointer;"
97 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
98 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
99 + aria-label="Cerrar menú"
100 + >
101 + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
102 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
103 + </svg>
104 + </button>
105 + <!-- Tooltip del botón cerrar -->
106 + <div class="absolute left-1/2 -translate-x-1/2 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
107 + Cerrar menú
108 + <div class="absolute left-1/2 -translate-x-1/2 -top-1 w-2 h-2 bg-gray-900/90 rotate-45"></div>
109 + </div>
110 + </div>
111 +
112 + <!-- Logo centrado (cambia según tema) -->
113 + <div class="flex flex-col items-center pt-8">
114 + {#if isDarkMode}
115 + <img
116 + src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png"
117 + alt="MEFP"
118 + class="h-16 md:h-20 w-auto mb-3"
119 + />
120 + {:else}
121 + <img
122 + src="/logos_claro/06_Isologo MEFP horizontal escala de grises.png"
123 + alt="MEFP"
124 + class="h-16 md:h-20 w-auto mb-3"
125 + />
126 + {/if}
127 + </div>
128 + </div>
129 +
130 + <!-- Navigation -->
131 + <nav class="flex-1 overflow-y-auto p-4">
132 + <!-- Inicio -->
133 + <a
134 + href="/"
135 + onclick={closeDrawer}
136 + class="w-full flex items-center gap-3 px-3 py-3 rounded-lg transition-colors text-left mb-4"
137 + style="background-color: {$page.url.pathname === '/' ? 'var(--theme-fill)' : 'transparent'};"
138 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'}
139 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = $page.url.pathname === '/' ? 'var(--theme-fill)' : 'transparent'}
140 + >
141 + <svg class="w-4 h-4 flex-shrink-0" style="color: var(--theme-texto);" fill="currentColor" viewBox="0 0 20 20">
142 + <rect x="2" y="2" width="7" height="7" rx="1.5"/>
143 + <rect x="11" y="2" width="7" height="7" rx="1.5"/>
144 + <rect x="2" y="11" width="7" height="7" rx="1.5"/>
145 + <rect x="11" y="11" width="7" height="7" rx="1.5"/>
146 + </svg>
147 + <span class="font-medium text-sm" style="color: var(--theme-titulo);">Página principal</span>
148 + </a>
149 +
150 + <!-- Clasificadores -->
151 + <div class="mb-6">
152 + <h3 class="text-xs font-semibold uppercase tracking-wider mb-3 px-2" style="color: var(--theme-texto);">
153 + Clasificadores
154 + </h3>
155 + <div class="space-y-1">
156 + {#each clasificadores as c}
157 + <a
158 + href={c.href}
159 + onclick={closeDrawer}
160 + class="w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-colors text-left group"
161 + style="background-color: {isActive(c.href) ? 'var(--theme-fill)' : 'transparent'};"
162 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'}
163 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = isActive(c.href) ? 'var(--theme-fill)' : 'transparent'}
164 + >
165 + <div class="flex-1 min-w-0">
166 + <span class="text-sm font-medium block" style="color: var(--theme-titulo);">{c.nombre}</span>
167 + <span class="text-xs" style="color: var(--theme-texto);">{c.desc}</span>
168 + </div>
169 + <svg
170 + class="w-4 h-4 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
171 + style="color: var(--theme-texto);"
172 + fill="none"
173 + stroke="currentColor"
174 + viewBox="0 0 24 24"
175 + stroke-width="1.5"
176 + >
177 + <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
178 + </svg>
179 + </a>
180 + {/each}
181 + </div>
182 + </div>
183 +
184 + <!-- Ver todos los clasificadores -->
185 + <a
186 + href="/clasificadores"
187 + onclick={closeDrawer}
188 + class="w-full flex items-center gap-3 px-3 py-3 rounded-lg transition-colors text-left mb-6"
189 + style="background-color: {$page.url.pathname === '/clasificadores' ? 'var(--theme-fill)' : 'transparent'};"
190 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'}
191 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = $page.url.pathname === '/clasificadores' ? 'var(--theme-fill)' : 'transparent'}
192 + >
193 + <svg class="w-4 h-4 flex-shrink-0" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
194 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"/>
195 + </svg>
196 + <span class="text-sm" style="color: var(--theme-accent);">Ver todos los clasificadores</span>
197 + </a>
198 +
199 + <!-- Separador -->
200 + <div class="border-t my-4" style="border-color: var(--theme-borde);"></div>
201 +
202 + <!-- Enlaces secundarios -->
203 + <div class="space-y-1">
204 + <a
205 + href="/metodologia"
206 + onclick={closeDrawer}
207 + class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-left"
208 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'}
209 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
210 + >
211 + <svg class="w-4 h-4 flex-shrink-0" style="color: var(--theme-texto);" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
212 + <path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"/>
213 + </svg>
214 + <span class="text-sm" style="color: var(--theme-titulo);">Metodología</span>
215 + </a>
216 + <a
217 + href="/acerca"
218 + onclick={closeDrawer}
219 + class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors text-left"
220 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'}
221 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
222 + >
223 + <svg class="w-4 h-4 flex-shrink-0" style="color: var(--theme-texto);" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
224 + <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"/>
225 + </svg>
226 + <span class="text-sm" style="color: var(--theme-titulo);">Acerca de</span>
227 + </a>
228 + </div>
229 + </nav>
230 +
231 + <!-- Footer -->
232 + <div class="p-4 border-t flex-shrink-0" style="border-color: rgba(0, 0, 0, 0.05);">
233 + <p class="text-xs text-center" style="color: var(--theme-texto);">
234 + Ministerio de Economía y Finanzas Públicas
235 + </p>
236 + </div>
237 +</aside>
...@@ -10,12 +10,21 @@ ...@@ -10,12 +10,21 @@
10 error, 10 error,
11 selectedIndex, 11 selectedIndex,
12 showResults, 12 showResults,
13 + filterType,
13 initIndex, 14 initIndex,
14 performSearch, 15 performSearch,
15 clearSearch, 16 clearSearch,
16 - navigateResults 17 + navigateResults,
18 + setFilter
17 } from '$lib/stores/searchStore'; 19 } from '$lib/stores/searchStore';
18 20
21 + const filterOptions = [
22 + { value: 'todos', label: 'Todos' },
23 + { value: 'entidad', label: 'Entidades' },
24 + { value: 'objeto_gasto', label: 'Objetos' },
25 + { value: 'rubro', label: 'Rubros' }
26 + ];
27 +
19 let inputRef; 28 let inputRef;
20 let debounceTimer; 29 let debounceTimer;
21 let isFocused = $state(false); 30 let isFocused = $state(false);
...@@ -77,16 +86,28 @@ ...@@ -77,16 +86,28 @@
77 <svelte:window onclick={handleClickOutside} /> 86 <svelte:window onclick={handleClickOutside} />
78 87
79 <div class="search-container relative"> 88 <div class="search-container relative">
80 - <input 89 + <div class="flex gap-2">
81 - bind:this={inputRef} 90 + <input
82 - type="text" 91 + bind:this={inputRef}
83 - placeholder={$indexLoaded ? "Buscar entidad, objeto de gasto o rubro..." : "Cargando..."} 92 + type="text"
84 - disabled={!$indexLoaded} 93 + placeholder={$indexLoaded ? "Buscar..." : "Cargando..."}
85 - class="w-full p-2 border rounded" 94 + disabled={!$indexLoaded}
86 - oninput={handleInput} 95 + class="flex-1 p-2 border rounded"
87 - onkeydown={handleKeydown} 96 + oninput={handleInput}
88 - onfocus={() => isFocused = true} 97 + onkeydown={handleKeydown}
89 - /> 98 + onfocus={() => isFocused = true}
99 + />
100 + <select
101 + class="px-3 py-2 border rounded bg-white text-sm text-slate-600 cursor-pointer"
102 + value={$filterType}
103 + onchange={(e) => setFilter(e.target.value)}
104 + disabled={!$indexLoaded}
105 + >
106 + {#each filterOptions as option}
107 + <option value={option.value}>{option.label}</option>
108 + {/each}
109 + </select>
110 + </div>
90 111
91 {#if $isLoading} 112 {#if $isLoading}
92 <span class="absolute right-2 top-2 text-sm text-gray-400">...</span> 113 <span class="absolute right-2 top-2 text-sm text-gray-400">...</span>
......
1 <script> 1 <script>
2 import { goto } from '$app/navigation'; 2 import { goto } from '$app/navigation';
3 - import { clearSearch } from '$lib/stores/searchStore'; 3 + import { clearSearch, filterType } from '$lib/stores/searchStore';
4 4
5 let { results = [], query = '', selectedIndex = -1 } = $props(); 5 let { results = [], query = '', selectedIndex = -1 } = $props();
6 6
...@@ -23,6 +23,9 @@ ...@@ -23,6 +23,9 @@
23 return { entidades, objetos, rubros }; 23 return { entidades, objetos, rubros };
24 }); 24 });
25 25
26 + // Si hay un filtro específico, no mostrar headers de grupo
27 + let showGroupHeaders = $derived($filterType === 'todos');
28 +
26 // Calcular índice global para navegación con teclado 29 // Calcular índice global para navegación con teclado
27 function getGlobalIndex(tipo, localIndex) { 30 function getGlobalIndex(tipo, localIndex) {
28 const g = groupedResults(); 31 const g = groupedResults();
...@@ -39,9 +42,11 @@ ...@@ -39,9 +42,11 @@
39 {#if results.length > 0} 42 {#if results.length > 0}
40 <div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded shadow-lg max-h-80 overflow-y-auto z-50"> 43 <div class="absolute top-full left-0 right-0 mt-1 bg-white border rounded shadow-lg max-h-80 overflow-y-auto z-50">
41 {#if groupedResults().entidades.length > 0} 44 {#if groupedResults().entidades.length > 0}
42 - <div class="px-3 py-1.5 bg-slate-50 border-b"> 45 + {#if showGroupHeaders}
43 - <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Entidades</span> 46 + <div class="px-3 py-1.5 bg-slate-50 border-b">
44 - </div> 47 + <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Entidades</span>
48 + </div>
49 + {/if}
45 {#each groupedResults().entidades as item, index} 50 {#each groupedResults().entidades as item, index}
46 {@const globalIdx = getGlobalIndex('entidad', index)} 51 {@const globalIdx = getGlobalIndex('entidad', index)}
47 <button 52 <button
...@@ -61,9 +66,11 @@ ...@@ -61,9 +66,11 @@
61 {/if} 66 {/if}
62 67
63 {#if groupedResults().objetos.length > 0} 68 {#if groupedResults().objetos.length > 0}
64 - <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 ? 'border-t' : ''}"> 69 + {#if showGroupHeaders}
65 - <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Objetos del Gasto</span> 70 + <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 ? 'border-t' : ''}">
66 - </div> 71 + <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Objetos del Gasto</span>
72 + </div>
73 + {/if}
67 {#each groupedResults().objetos as item, index} 74 {#each groupedResults().objetos as item, index}
68 {@const globalIdx = getGlobalIndex('objeto_gasto', index)} 75 {@const globalIdx = getGlobalIndex('objeto_gasto', index)}
69 <button 76 <button
...@@ -81,9 +88,11 @@ ...@@ -81,9 +88,11 @@
81 {/if} 88 {/if}
82 89
83 {#if groupedResults().rubros.length > 0} 90 {#if groupedResults().rubros.length > 0}
84 - <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 || groupedResults().objetos.length > 0 ? 'border-t' : ''}"> 91 + {#if showGroupHeaders}
85 - <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Rubros</span> 92 + <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 || groupedResults().objetos.length > 0 ? 'border-t' : ''}">
86 - </div> 93 + <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Rubros</span>
94 + </div>
95 + {/if}
87 {#each groupedResults().rubros as item, index} 96 {#each groupedResults().rubros as item, index}
88 {@const globalIdx = getGlobalIndex('rubro', index)} 97 {@const globalIdx = getGlobalIndex('rubro', index)}
89 <button 98 <button
......
1 <script> 1 <script>
2 - import { page } from '$app/stores'; 2 + import { onMount } from 'svelte';
3 +
4 + let { onOpenDrawer = () => {} } = $props();
5 +
6 + let isDark = $state(false);
7 +
8 + function toggleTheme() {
9 + isDark = !isDark;
10 + document.documentElement.classList.toggle('dark', isDark);
11 + localStorage.setItem('theme', isDark ? 'dark' : 'light');
12 + }
13 +
14 + onMount(() => {
15 + // Read actual state from document (set by app.html)
16 + isDark = document.documentElement.classList.contains('dark');
17 +
18 + // Listen for theme changes (e.g., from drawer on mobile)
19 + const observer = new MutationObserver(() => {
20 + isDark = document.documentElement.classList.contains('dark');
21 + });
22 + observer.observe(document.documentElement, {
23 + attributes: true,
24 + attributeFilter: ['class']
25 + });
26 +
27 + return () => observer.disconnect();
28 + });
3 </script> 29 </script>
4 30
5 -<nav class="border-b p-4"> 31 +<!-- MÓVIL: Solo hamburguesa arriba a la derecha -->
6 - <div class="max-w-4xl mx-auto flex items-center justify-between"> 32 +<div class="fixed top-4 right-4 z-[110] md:hidden">
7 - <a href="/">Presupuesto Público Bolivia</a> 33 + <button
8 - <div class="flex gap-4 text-sm"> 34 + onclick={onOpenDrawer}
9 - <a href="/">Inicio</a> 35 + class="p-2 rounded-full transition-all flex-shrink-0"
10 - <a href="/clasificadores">Clasificadores</a> 36 + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;"
37 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
38 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
39 + aria-label="Abrir menú"
40 + >
41 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
42 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
43 + </svg>
44 + </button>
45 +</div>
46 +
47 +<!-- DESKTOP: Barra completa con Home + Theme + Menu -->
48 +<div class="hidden md:flex fixed top-6 right-6 z-[110]">
49 + <div class="flex items-center gap-1">
50 +
51 + <!-- BOTÓN HOME (4 cuadrados) -->
52 + <div class="relative group/home">
53 + <a
54 + href="/"
55 + class="p-2 rounded-full transition-all block"
56 + style="background-color: transparent; color: var(--theme-texto);"
57 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
58 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
59 + >
60 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
61 + <rect x="2" y="2" width="7" height="7" rx="1.5"/>
62 + <rect x="11" y="2" width="7" height="7" rx="1.5"/>
63 + <rect x="2" y="11" width="7" height="7" rx="1.5"/>
64 + <rect x="11" y="11" width="7" height="7" rx="1.5"/>
65 + </svg>
66 + </a>
67 + <!-- Tooltip -->
68 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/home:opacity-100 group-hover/home:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
69 + Página principal
70 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
71 + </div>
72 + </div>
73 +
74 + <!-- BOTÓN THEME TOGGLE (Sol/Luna) -->
75 + <div class="relative group/theme">
76 + <button
77 + onclick={toggleTheme}
78 + class="p-2 rounded-full transition-all"
79 + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;"
80 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
81 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
82 + aria-label={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
83 + >
84 + {#if isDark}
85 + <!-- LUNA (modo oscuro activo) -->
86 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
87 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
88 + </svg>
89 + {:else}
90 + <!-- SOL (modo claro activo) -->
91 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
92 + <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
93 + </svg>
94 + {/if}
95 + </button>
96 + <!-- Tooltip -->
97 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/theme:opacity-100 group-hover/theme:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
98 + {isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
99 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
100 + </div>
11 </div> 101 </div>
102 +
103 + <!-- BOTÓN MENÚ HAMBURGUESA -->
104 + <div class="relative group/menu">
105 + <button
106 + onclick={onOpenDrawer}
107 + class="p-2 rounded-full transition-all"
108 + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;"
109 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
110 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
111 + aria-label="Abrir menú"
112 + >
113 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
114 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
115 + </svg>
116 + </button>
117 + <!-- Tooltip -->
118 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/menu:opacity-100 group-hover/menu:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
119 + Menú
120 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
121 + </div>
122 + </div>
123 +
12 </div> 124 </div>
13 -</nav> 125 +</div>
......
1 +import { writable } from 'svelte/store';
2 +
3 +export const drawerOpen = writable(false);
4 +
5 +export function openDrawer() {
6 + drawerOpen.set(true);
7 +}
8 +
9 +export function closeDrawer() {
10 + drawerOpen.set(false);
11 +}
12 +
13 +export function toggleDrawer() {
14 + drawerOpen.update(v => !v);
15 +}
1 -import { writable, derived } from 'svelte/store'; 1 +import { writable, derived, get } from 'svelte/store';
2 import { initSearchIndex, search, isIndexReady } from '$lib/services/search'; 2 import { initSearchIndex, search, isIndexReady } from '$lib/services/search';
3 3
4 // Estado del buscador 4 // Estado del buscador
...@@ -8,6 +8,7 @@ export const isLoading = writable(false); ...@@ -8,6 +8,7 @@ export const isLoading = writable(false);
8 export const indexLoaded = writable(false); 8 export const indexLoaded = writable(false);
9 export const error = writable(null); 9 export const error = writable(null);
10 export const selectedIndex = writable(-1); 10 export const selectedIndex = writable(-1);
11 +export const filterType = writable('todos'); // todos, entidad, objeto_gasto, rubro
11 12
12 // Estado derivado: si está mostrando resultados 13 // Estado derivado: si está mostrando resultados
13 export const showResults = derived( 14 export const showResults = derived(
...@@ -41,7 +42,7 @@ export async function initIndex() { ...@@ -41,7 +42,7 @@ export async function initIndex() {
41 /** 42 /**
42 * Realiza la búsqueda 43 * Realiza la búsqueda
43 */ 44 */
44 -export function performSearch(searchQuery) { 45 +export function performSearch(searchQuery, filter = null) {
45 query.set(searchQuery); 46 query.set(searchQuery);
46 selectedIndex.set(-1); 47 selectedIndex.set(-1);
47 48
...@@ -50,8 +51,16 @@ export function performSearch(searchQuery) { ...@@ -50,8 +51,16 @@ export function performSearch(searchQuery) {
50 return; 51 return;
51 } 52 }
52 53
53 - const searchResults = search(searchQuery, 15); 54 + let searchResults = search(searchQuery, 30);
54 - results.set(searchResults); 55 +
56 + // Filtrar por tipo si no es "todos"
57 + const currentFilter = filter !== null ? filter : get(filterType);
58 + if (currentFilter !== 'todos') {
59 + searchResults = searchResults.filter(r => r.tipo === currentFilter);
60 + }
61 +
62 + // Limitar resultados
63 + results.set(searchResults.slice(0, 15));
55 } 64 }
56 65
57 /** 66 /**
...@@ -64,6 +73,18 @@ export function clearSearch() { ...@@ -64,6 +73,18 @@ export function clearSearch() {
64 } 73 }
65 74
66 /** 75 /**
76 + * Cambia el filtro de tipo
77 + */
78 +export function setFilter(type) {
79 + filterType.set(type);
80 + // Re-ejecutar búsqueda con el nuevo filtro
81 + const currentQuery = get(query);
82 + if (currentQuery.length >= 2) {
83 + performSearch(currentQuery, type);
84 + }
85 +}
86 +
87 +/**
67 * Navegación por teclado 88 * Navegación por teclado
68 */ 89 */
69 export function navigateResults(direction, totalResults) { 90 export function navigateResults(direction, totalResults) {
......
1 <script> 1 <script>
2 import '../app.css'; 2 import '../app.css';
3 + import { page } from '$app/stores';
3 import favicon from '$lib/assets/favicon.svg'; 4 import favicon from '$lib/assets/favicon.svg';
4 import Navbar from '$lib/components/ui/Navbar.svelte'; 5 import Navbar from '$lib/components/ui/Navbar.svelte';
6 + import NavigationDrawer from '$lib/components/layout/NavigationDrawer.svelte';
7 + import { openDrawer } from '$lib/stores/drawer.js';
5 8
6 let { children } = $props(); 9 let { children } = $props();
10 +
11 + // Don't show navbar on landing page
12 + let isLanding = $derived($page.url.pathname === '/');
7 </script> 13 </script>
8 14
9 <svelte:head> 15 <svelte:head>
10 <link rel="icon" href={favicon} /> 16 <link rel="icon" href={favicon} />
17 + <link rel="preconnect" href="https://fonts.googleapis.com" />
18 + <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" />
11 </svelte:head> 19 </svelte:head>
12 20
13 -<div class="min-h-screen flex flex-col"> 21 +<!-- Navigation Drawer (siempre presente) -->
14 - <Navbar /> 22 +<NavigationDrawer />
15 - <main class="flex-1"> 23 +
24 +<!-- Floating controls bar (fixed position - no afecta el flujo) -->
25 +{#if !isLanding}
26 + <Navbar onOpenDrawer={openDrawer} />
27 +{/if}
28 +
29 +<div class="min-h-screen" style="background-color: var(--theme-body);">
30 + <main>
16 {@render children()} 31 {@render children()}
17 </main> 32 </main>
18 </div> 33 </div>
......
1 <script> 1 <script>
2 - import SearchBox from '$lib/components/search/SearchBox.svelte'; 2 + import { onMount } from 'svelte';
3 + import { goto } from '$app/navigation';
4 + import { openDrawer } from '$lib/stores/drawer.js';
5 + import {
6 + query,
7 + results,
8 + isLoading,
9 + indexLoaded,
10 + error,
11 + selectedIndex,
12 + showResults,
13 + initIndex,
14 + performSearch,
15 + clearSearch,
16 + navigateResults
17 + } from '$lib/stores/searchStore';
18 +
19 + // State - default to archivo (historical mode)
20 + let mode = $state('archivo');
21 + let searchFocused = $state(false);
22 + let hoveredStory = $state(null);
23 + let mousePos = $state({ x: 0, y: 0 });
24 + let activeToggle = $state(null);
25 + let searchInput = $state(null);
26 + let debounceTimer;
27 + let isDarkMode = $state(true);
28 +
29 + // Visibility tracking for reveal animations
30 + let revealStates = $state({});
31 +
32 + // Dynamic colors that respond to theme
33 + // We'll use CSS variables but keep these for JS-driven effects
34 + const COLORS = {
35 + accent: '#C9A751',
36 + accentDim: '#B89A2A',
37 + red: '#C44B3F',
38 + green: '#4A8B6E',
39 + };
40 +
41 + const MODES = {
42 + archivo: {
43 + placeholder: 'YPFB, Publicidad, Sueldos...',
44 + label: 'Serie histórica',
45 + color: COLORS.accent,
46 + },
47 + hoy: {
48 + placeholder: 'Tu municipio, Salud, Obras...',
49 + label: 'Actualización diaria',
50 + color: COLORS.green,
51 + },
52 + };
53 +
54 + const STORIES = [
55 + { tag: 'INVESTIGACION', accent: COLORS.accent, title: '¿Cuanto gasta Bolivia en consultorias cada año?', desc: 'Un analisis de 20 años de contratos de consultoria revela patrones que todo ciudadano deberia conocer.' },
56 + { tag: 'TRANSPARENCIA', accent: COLORS.green, title: 'El presupuesto de tu municipio, peso por peso', desc: 'Cada boliviano invertido en tu municipio, rastreado desde su asignacion hasta su ejecucion final.' },
57 + { tag: 'DATOS', accent: COLORS.red, title: 'Salud vs. Defensa: 20 años de prioridades', desc: 'Como han cambiado las prioridades presupuestarias de Bolivia a lo largo de dos decadas.' },
58 + ];
59 +
60 + let cur = $derived(MODES[mode]);
61 +
62 + function handleSwitch(m) {
63 + mode = m;
64 + clearSearch();
65 + if (searchInput) searchInput.value = '';
66 + setTimeout(() => searchInput?.focus(), 150);
67 + }
68 +
69 + function handleToggleHover(m) {
70 + activeToggle = m;
71 + }
72 +
73 + function handleToggleLeave() {
74 + activeToggle = null;
75 + }
76 +
77 + function handleBlur() {
78 + setTimeout(() => { searchFocused = false; }, 200);
79 + }
80 +
81 + function handleInput(e) {
82 + const value = e.target.value;
83 + clearTimeout(debounceTimer);
84 + debounceTimer = setTimeout(() => {
85 + performSearch(value);
86 + }, 250);
87 + }
88 +
89 + function handleKeydown(e) {
90 + if (!$showResults) return;
91 +
92 + switch (e.key) {
93 + case 'ArrowDown':
94 + e.preventDefault();
95 + navigateResults('down', $results.length);
96 + break;
97 + case 'ArrowUp':
98 + e.preventDefault();
99 + navigateResults('up', $results.length);
100 + break;
101 + case 'Enter':
102 + e.preventDefault();
103 + if ($selectedIndex >= 0 && $results[$selectedIndex]) {
104 + handleSelect($results[$selectedIndex]);
105 + }
106 + break;
107 + case 'Escape':
108 + clearSearch();
109 + if (searchInput) searchInput.value = '';
110 + searchInput?.blur();
111 + break;
112 + }
113 + }
114 +
115 + function handleSelect(item) {
116 + if (item.tipo === 'entidad') {
117 + goto(`/entidad/${item.codigo}`);
118 + } else if (item.tipo === 'objeto_gasto') {
119 + goto(`/objeto/${item.codigo}`);
120 + } else if (item.tipo === 'rubro') {
121 + goto(`/rubro/${item.codigo}`);
122 + }
123 + clearSearch();
124 + if (searchInput) searchInput.value = '';
125 + }
126 +
127 + function getTypeLabel(tipo) {
128 + switch (tipo) {
129 + case 'entidad': return 'ENTIDAD';
130 + case 'objeto_gasto': return 'OBJETO DE GASTO';
131 + case 'rubro': return 'RUBRO';
132 + default: return tipo.toUpperCase();
133 + }
134 + }
135 +
136 + // Intersection observer for reveals
137 + function reveal(node, { id, delay = 0 }) {
138 + revealStates[id] = false;
139 + revealStates = revealStates;
140 +
141 + const observer = new IntersectionObserver(([entry]) => {
142 + if (entry.isIntersecting) {
143 + setTimeout(() => {
144 + revealStates[id] = true;
145 + revealStates = revealStates;
146 + }, delay);
147 + observer.disconnect();
148 + }
149 + }, { threshold: 0.15 });
150 +
151 + observer.observe(node);
152 + return { destroy() { observer.disconnect(); } };
153 + }
154 +
155 + function toggleTheme() {
156 + isDarkMode = !isDarkMode;
157 + document.documentElement.classList.toggle('dark', isDarkMode);
158 + localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
159 + }
160 +
161 + onMount(() => {
162 + // Initialize search index
163 + initIndex();
164 +
165 + // Check initial theme
166 + isDarkMode = document.documentElement.classList.contains('dark');
167 +
168 + // Listen for theme changes
169 + const observer = new MutationObserver((mutations) => {
170 + mutations.forEach((mutation) => {
171 + if (mutation.attributeName === 'class') {
172 + isDarkMode = document.documentElement.classList.contains('dark');
173 + }
174 + });
175 + });
176 + observer.observe(document.documentElement, { attributes: true });
177 +
178 + const handleMouse = (e) => { mousePos = { x: e.clientX, y: e.clientY }; };
179 + window.addEventListener('mousemove', handleMouse);
180 + return () => {
181 + window.removeEventListener('mousemove', handleMouse);
182 + observer.disconnect();
183 + };
184 + });
3 </script> 185 </script>
4 186
5 <svelte:head> 187 <svelte:head>
6 - <title>Presupuesto Público Bolivia</title> 188 + <title>Presupuesto Abierto — Bolivia</title>
7 </svelte:head> 189 </svelte:head>
8 190
9 -<div class="max-w-4xl mx-auto p-4"> 191 +<div class="page">
10 - <h1 class="text-2xl mb-6">Presupuesto Público de Bolivia</h1> 192 + <!-- Grain overlay -->
11 - 193 + <div class="grain"></div>
12 - <div class="mb-8"> 194 +
13 - <SearchBox /> 195 + <!-- Header con Logo y Controles -->
14 - </div> 196 + <header class="page-header">
15 - 197 + {#if isDarkMode}
16 - <div class="grid grid-cols-2 gap-4 text-sm"> 198 + <img
17 - <a href="/clasificadores" class="p-4 border rounded hover:bg-gray-50"> 199 + src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png"
18 - <strong>Clasificadores</strong> 200 + alt="Ministerio de Economia y Finanzas Publicas"
19 - <p class="text-gray-600">Navegar por instituciones, objetos de gasto, etc.</p> 201 + class="mefp-logo"
20 - </a> 202 + />
21 - <a href="/historias" class="p-4 border rounded hover:bg-gray-50"> 203 + {:else}
22 - <strong>Historias</strong> 204 + <img
23 - <p class="text-gray-600">Análisis y visualizaciones del gasto público</p> 205 + src="/logos_claro/06_Isologo MEFP horizontal escala de grises.png"
24 - </a> 206 + alt="Ministerio de Economia y Finanzas Publicas"
25 - </div> 207 + class="mefp-logo"
208 + />
209 + {/if}
210 +
211 + <!-- Controles: Theme Toggle + Menú -->
212 + <div class="flex items-center gap-1">
213 + <!-- Theme Toggle (solo desktop) -->
214 + <div class="relative group/theme hidden md:block">
215 + <button
216 + onclick={toggleTheme}
217 + class="p-2 rounded-full transition-all"
218 + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;"
219 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
220 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
221 + aria-label={isDarkMode ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
222 + >
223 + {#if isDarkMode}
224 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
225 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
226 + </svg>
227 + {:else}
228 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
229 + <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
230 + </svg>
231 + {/if}
232 + </button>
233 + <!-- Tooltip -->
234 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/theme:opacity-100 group-hover/theme:visible transition-all duration-200 pointer-events-none backdrop-blur-sm z-50">
235 + {isDarkMode ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
236 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
237 + </div>
238 + </div>
239 +
240 + <!-- Botón Menú -->
241 + <div class="relative group/menu">
242 + <button
243 + onclick={openDrawer}
244 + class="p-2 rounded-full transition-all"
245 + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;"
246 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'}
247 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--theme-texto)'}
248 + aria-label="Abrir menú"
249 + >
250 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
251 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
252 + </svg>
253 + </button>
254 + <!-- Tooltip -->
255 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/menu:opacity-100 group-hover/menu:visible transition-all duration-200 pointer-events-none backdrop-blur-sm z-50">
256 + Menú
257 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
258 + </div>
259 + </div>
260 + </div>
261 + </header>
262 +
263 + <!-- Hero -->
264 + <section class="hero">
265 + <div
266 + class="mouse-glow"
267 + style="background: radial-gradient(800px circle at {mousePos.x}px {mousePos.y}px, rgba(201, 167, 81, 0.03), transparent 60%)"
268 + ></div>
269 +
270 + <div class="hero-content">
271 + <!-- Main title -->
272 + <h1 class="hero-title fade-in" style="animation-delay: 0.3s">
273 + Presupuesto Abierto
274 + </h1>
275 +
276 + <!-- Subtitle - the scale -->
277 + <p class="hero-subtitle fade-in" style="animation-delay: 0.5s">
278 + Todos los ingresos y gastos del Estado boliviano.<br />
279 + <span class="subtitle-stats">600+ entidades · 500+ rubros · 21 años · 9M de registros · actualización diaria</span>
280 + </p>
281 +
282 + <!-- Search - main CTA -->
283 + <div class="search-wrapper fade-in" style="animation-delay: 0.6s">
284 + <div
285 + class="search-bar"
286 + class:search-focused={searchFocused}
287 + class:search-has-results={searchFocused && $results.length > 0}
288 + >
289 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none"
290 + class="search-icon"
291 + class:search-icon-focused={searchFocused}
292 + stroke-width="2" stroke-linecap="round">
293 + <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/>
294 + </svg>
295 + <input
296 + bind:this={searchInput}
297 + type="text"
298 + placeholder={$indexLoaded ? cur.placeholder : "Cargando indice..."}
299 + disabled={!$indexLoaded}
300 + oninput={handleInput}
301 + onkeydown={handleKeydown}
302 + onfocus={() => searchFocused = true}
303 + onblur={handleBlur}
304 + class="search-input"
305 + />
306 +
307 + {#if $query.length > 0}
308 + <button class="clear-btn" onclick={() => { clearSearch(); if (searchInput) searchInput.value = ''; }}>x</button>
309 + {/if}
310 + </div>
311 +
312 + <!-- Loading indicator -->
313 + {#if $isLoading}
314 + <div class="search-loading">Buscando...</div>
315 + {/if}
316 +
317 + <!-- Results dropdown -->
318 + {#if searchFocused && $results.length > 0}
319 + <div class="suggestions">
320 + {#each $results as item, i}
321 + <button
322 + type="button"
323 + class="suggestion-item"
324 + class:suggestion-border={i > 0}
325 + class:suggestion-selected={$selectedIndex === i}
326 + onclick={() => handleSelect(item)}
327 + >
328 + <div class="suggestion-main">
329 + <span class="suggestion-label">{item.nombre}</span>
330 + {#if item.sigla}
331 + <span class="suggestion-sigla">({item.sigla})</span>
332 + {/if}
333 + </div>
334 + <div class="suggestion-meta">
335 + {#if item.codigo}
336 + <span class="suggestion-code">{item.codigo}</span>
337 + {/if}
338 + <span class="suggestion-type">{getTypeLabel(item.tipo)}</span>
339 + </div>
340 + </button>
341 + {/each}
342 + </div>
343 + {:else if searchFocused && $query.length >= 2 && $results.length === 0 && !$isLoading}
344 + <div class="suggestions no-results">
345 + <div class="no-results-text">Sin resultados para "{$query}"</div>
346 + </div>
347 + {/if}
348 +
349 + <!-- Error message -->
350 + {#if $error}
351 + <div class="search-error">{$error}</div>
352 + {/if}
353 +
354 + {#if !searchFocused || ($results.length === 0 && $query.length < 2)}
355 + <a href="/clasificadores" class="browse-link">
356 + o explora por clasificadores <span class="browse-arrow">→</span>
357 + </a>
358 + {/if}
359 + </div>
360 +
361 + <!-- Mode toggle -->
362 + <div class="mode-toggle-wrapper fade-in" style="animation-delay: 0.8s">
363 + <span class="toggle-hint">Elige el periodo</span>
364 + <div class="mode-toggle">
365 + <button
366 + type="button"
367 + class="toggle-btn"
368 + class:toggle-active={mode === 'archivo'}
369 + class:toggle-hover={activeToggle === 'archivo' && mode !== 'archivo'}
370 + onclick={() => handleSwitch('archivo')}
371 + onmouseenter={() => handleToggleHover('archivo')}
372 + onmouseleave={handleToggleLeave}
373 + >
374 + <span class="toggle-label">Serie histórica</span>
375 + <span class="toggle-years">2005–2025</span>
376 + </button>
377 +
378 + <span class="toggle-divider"></span>
379 +
380 + <button
381 + type="button"
382 + class="toggle-btn"
383 + class:toggle-active={mode === 'hoy'}
384 + class:toggle-hover={activeToggle === 'hoy' && mode !== 'hoy'}
385 + onclick={() => handleSwitch('hoy')}
386 + onmouseenter={() => handleToggleHover('hoy')}
387 + onmouseleave={handleToggleLeave}
388 + >
389 + <span class="toggle-label">Actualización diaria</span>
390 + <span class="toggle-years">2025</span>
391 + </button>
392 + </div>
393 + </div>
394 + </div>
395 +
396 + <!-- Scroll indicator -->
397 + <div class="scroll-indicator fade-in" style="animation-delay: 1.2s">
398 + <span>SCROLL</span>
399 + <div class="scroll-line"></div>
400 + </div>
401 + </section>
402 +
403 + <!-- Stories -->
404 + <section class="stories-section" id="historias">
405 + <div
406 + use:reveal={{ id: 'stories-header' }}
407 + class="stories-header"
408 + class:revealed={revealStates['stories-header']}
409 + >
410 + <div>
411 + <h2 class="stories-title-big">Lecturas del presupuesto</h2>
412 + </div>
413 + <a href="/historias" class="stories-link">Ver todas →</a>
414 + </div>
415 +
416 + <div class="stories-grid">
417 + {#each STORIES as story, i}
418 + <div
419 + use:reveal={{ id: `story-${i}`, delay: i * 150 }}
420 + class="story-card"
421 + class:revealed={revealStates[`story-${i}`]}
422 + class:story-hovered={hoveredStory === i}
423 + onmouseenter={() => hoveredStory = i}
424 + onmouseleave={() => hoveredStory = null}
425 + role="article"
426 + >
427 + <div class="story-accent-line" style="background: {story.accent}; transform: scaleX({hoveredStory === i ? 1 : 0})"></div>
428 + <div class="story-content">
429 + <span class="story-tag" style="color: {story.accent}">{story.tag}</span>
430 + <h3 class="story-card-title">{story.title}</h3>
431 + <p class="story-desc">{story.desc}</p>
432 + </div>
433 + <div class="story-cta" class:story-cta-active={hoveredStory === i}>
434 + Leer historia <span class="story-arrow" class:arrow-moved={hoveredStory === i}>→</span>
435 + </div>
436 + </div>
437 + {/each}
438 + </div>
439 + </section>
26 </div> 440 </div>
441 +
442 +<style>
443 + @import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,300;8..60,400;8..60,600;8..60,700;8..60,900&family=JetBrains+Mono:wght@300;400;500&family=DM+Sans:wght@400;500;600;700&display=swap');
444 +
445 + .page * { margin: 0; padding: 0; box-sizing: border-box; }
446 + .page ::selection { background: var(--theme-accent); color: var(--theme-body); }
447 + .page::-webkit-scrollbar { width: 6px; }
448 + .page::-webkit-scrollbar-track { background: var(--theme-body); }
449 + .page::-webkit-scrollbar-thumb { background: var(--theme-texto); border-radius: 3px; }
450 +
451 + @keyframes grain { 0%,100%{transform:translate(0,0)}10%{transform:translate(-2%,-3%)}30%{transform:translate(-1%,3%)}50%{transform:translate(-3%,1%)}70%{transform:translate(-2%,2%)}90%{transform:translate(-1%,2%)} }
452 + @keyframes fadeInUp { from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)} }
453 +
454 + .page {
455 + background: var(--theme-body);
456 + color: var(--theme-titulo);
457 + min-height: 100vh;
458 + font-family: 'Source Serif 4', Georgia, serif;
459 + overflow-x: hidden;
460 + transition: background-color 0.3s, color 0.3s;
461 + }
462 +
463 + /* Grain - only visible in dark mode */
464 + .grain {
465 + position: fixed; top: -50%; left: -50%; width: 200%; height: 200%;
466 + pointer-events: none; z-index: 9999; opacity: 0.03;
467 + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
468 + animation: grain 0.5s steps(1) infinite;
469 + }
470 + :global(:root:not(.dark)) .grain { display: none; }
471 +
472 + /* Header with MEFP logo */
473 + .page-header {
474 + position: fixed; top: 0; left: 0; right: 0; z-index: 100;
475 + padding: 24px 40px;
476 + background: linear-gradient(180deg, var(--theme-body) 0%, transparent 100%);
477 + display: flex; align-items: center; justify-content: space-between;
478 + }
479 + .mefp-logo {
480 + height: 80px; width: auto;
481 + opacity: 0.9;
482 + transition: opacity 0.3s;
483 + }
484 + .mefp-logo:hover { opacity: 1; }
485 +
486 + /* Fade in animation */
487 + .fade-in {
488 + opacity: 0;
489 + animation: fadeInUp 1s cubic-bezier(0.16, 1, 0.3, 1) forwards;
490 + animation-fill-mode: forwards;
491 + }
492 +
493 + /* Hero */
494 + .hero {
495 + min-height: 100vh; display: flex; flex-direction: column; justify-content: center;
496 + padding: 80px 40px 60px; position: relative; max-width: 1200px; margin: 0 auto;
497 + }
498 + .mouse-glow {
499 + position: fixed; top: 0; left: 0; right: 0; bottom: 0;
500 + pointer-events: none; z-index: 0; transition: background 0.6s;
501 + }
502 + :global(:root:not(.dark)) .mouse-glow { display: none; }
503 + .hero-content {
504 + position: relative; z-index: 1;
505 + display: flex; flex-direction: column; align-items: center;
506 + text-align: center;
507 + }
508 +
509 + /* Title */
510 + .hero-title {
511 + font-size: clamp(40px, 7vw, 76px); font-weight: 700;
512 + line-height: 1.05; letter-spacing: -0.02em;
513 + color: var(--theme-titulo);
514 + }
515 +
516 + /* Subtitle */
517 + .hero-subtitle {
518 + margin-top: 24px; max-width: 720px;
519 + font-size: clamp(17px, 2vw, 20px); line-height: 1.6;
520 + color: var(--theme-texto); font-weight: 300;
521 + }
522 + .subtitle-stats {
523 + display: block; margin-top: 12px;
524 + font-family: 'JetBrains Mono', monospace;
525 + font-size: clamp(11px, 1.3vw, 13px);
526 + letter-spacing: 0.08em;
527 + color: var(--theme-accent);
528 + }
529 +
530 + /* Mode toggle - equal hierarchy */
531 + .mode-toggle-wrapper {
532 + display: flex; flex-direction: column; align-items: center;
533 + margin-top: 56px; gap: 12px;
534 + position: relative; z-index: 10;
535 + }
536 + .toggle-hint {
537 + font-family: 'JetBrains Mono', monospace;
538 + font-size: 9px; letter-spacing: 0.15em;
539 + text-transform: uppercase; color: var(--theme-texto);
540 + opacity: 0.6;
541 + }
542 + .mode-toggle {
543 + display: flex; align-items: center; gap: 0;
544 + }
545 +
546 + .toggle-btn {
547 + display: flex; flex-direction: column; align-items: flex-start;
548 + padding: 10px 18px;
549 + background: transparent; border: 1px solid var(--theme-borde);
550 + cursor: pointer; transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
551 + }
552 +
553 + .toggle-btn:first-child {
554 + border-radius: 6px 0 0 6px;
555 + border-right: none;
556 + }
557 +
558 + .toggle-btn:last-child {
559 + border-radius: 0 6px 6px 0;
560 + }
561 +
562 + .toggle-btn.toggle-active {
563 + background: var(--theme-fill);
564 + border-color: var(--theme-borde);
565 + }
566 +
567 + .toggle-btn.toggle-hover {
568 + background: var(--theme-fill);
569 + border-color: var(--theme-borde);
570 + }
571 +
572 + .toggle-label {
573 + font-family: 'DM Sans', sans-serif; font-size: 12px;
574 + color: var(--theme-texto); font-weight: 500;
575 + transition: color 0.3s;
576 + }
577 +
578 + .toggle-btn.toggle-active .toggle-label {
579 + color: var(--theme-titulo);
580 + }
581 +
582 + .toggle-btn.toggle-hover .toggle-label {
583 + color: var(--theme-titulo);
584 + opacity: 0.7;
585 + }
586 +
587 + .toggle-years {
588 + font-family: 'JetBrains Mono', monospace; font-size: 9px;
589 + letter-spacing: 0.08em; color: var(--theme-texto);
590 + opacity: 0.5;
591 + margin-top: 2px; transition: color 0.3s;
592 + }
593 +
594 + .toggle-btn.toggle-active .toggle-years {
595 + color: var(--theme-texto);
596 + opacity: 0.8;
597 + }
598 +
599 + .toggle-divider {
600 + width: 1px; height: 36px; background: var(--theme-borde);
601 + }
602 +
603 + .browse-link {
604 + display: block;
605 + margin-top: 14px;
606 + font-family: 'DM Sans', sans-serif;
607 + font-size: 12px; color: var(--theme-texto);
608 + text-decoration: none;
609 + text-align: center;
610 + transition: color 0.2s;
611 + }
612 + .browse-link:hover { color: var(--theme-accent); }
613 + .browse-link:hover .browse-arrow { transform: translateX(3px); }
614 + .browse-arrow {
615 + display: inline-block;
616 + transition: transform 0.2s;
617 + }
618 +
619 + /* Search - main CTA */
620 + .search-wrapper { margin-top: 28px; width: 100%; max-width: 520px; position: relative; z-index: 60; }
621 + .search-bar {
622 + display: flex; align-items: center;
623 + background: var(--theme-surface); border: 1px solid var(--theme-borde);
624 + border-radius: 12px; padding: 6px 24px;
625 + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
626 + }
627 + .search-focused { background: var(--theme-fill); }
628 + .search-has-results { border-radius: 12px 12px 0 0; }
629 +
630 + .search-bar input {
631 + flex: 1; background: transparent; border: none; outline: none;
632 + color: var(--theme-titulo); font-size: 16px; font-family: 'DM Sans', sans-serif;
633 + padding: 16px 14px; caret-color: var(--theme-accent);
634 + }
635 + .search-bar input::placeholder { color: var(--theme-texto); opacity: 0.6; }
636 + .search-bar input:disabled { opacity: 0.5; cursor: not-allowed; }
637 +
638 + .search-icon { stroke: var(--theme-texto); transition: stroke 0.3s; }
639 + .search-icon-focused { stroke: var(--theme-accent); }
640 +
641 + .clear-btn {
642 + background: none; border: none; cursor: pointer; color: var(--theme-texto);
643 + font-size: 20px; padding: 4px 0 4px 8px; line-height: 1; transition: color 0.2s;
644 + }
645 + .clear-btn:hover { color: var(--theme-titulo); }
646 +
647 + .search-loading {
648 + position: absolute; top: 100%; left: 0; right: 0;
649 + background: var(--theme-surface); border: 1px solid var(--theme-borde); border-top: none;
650 + border-radius: 0 0 14px 14px; padding: 12px 24px;
651 + font-family: 'DM Sans', sans-serif; font-size: 13px; color: var(--theme-texto);
652 + }
653 +
654 + .search-error {
655 + margin-top: 8px; font-family: 'DM Sans', sans-serif;
656 + font-size: 13px; color: #C44B3F;
657 + }
658 +
659 + /* Suggestions / Results */
660 + .suggestions {
661 + position: absolute; top: 100%; left: 0; right: 0;
662 + background: var(--theme-surface); border: 1px solid var(--theme-borde); border-top: none;
663 + border-radius: 0 0 14px 14px; overflow: hidden; z-index: 100;
664 + max-height: 320px; overflow-y: auto;
665 + }
666 + .suggestions.no-results { padding: 16px 24px; }
667 + .no-results-text {
668 + font-family: 'DM Sans', sans-serif; font-size: 14px; color: var(--theme-texto);
669 + }
670 +
671 + .suggestion-item {
672 + width: 100%; text-align: left; background: none; border: none;
673 + padding: 12px 24px; display: flex; justify-content: space-between;
674 + align-items: center; cursor: pointer; transition: background 0.2s;
675 + }
676 + .suggestion-item:hover, .suggestion-selected { background: var(--theme-fill); }
677 + .suggestion-border { border-top: 1px solid var(--theme-borde); }
678 +
679 + .suggestion-main { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
680 + .suggestion-label {
681 + font-family: 'DM Sans', sans-serif; font-size: 14px; color: var(--theme-titulo);
682 + white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
683 + }
684 + .suggestion-sigla {
685 + font-family: 'DM Sans', sans-serif; font-size: 12px; color: var(--theme-texto);
686 + flex-shrink: 0;
687 + }
688 +
689 + .suggestion-meta { display: flex; align-items: center; gap: 8px; flex-shrink: 0; margin-left: 12px; }
690 + .suggestion-code {
691 + font-family: 'JetBrains Mono', monospace; font-size: 11px;
692 + color: var(--theme-texto); background: var(--theme-body); padding: 2px 6px; border-radius: 3px;
693 + }
694 + .suggestion-type {
695 + font-family: 'JetBrains Mono', monospace; font-size: 10px;
696 + letter-spacing: 0.1em; color: var(--theme-texto); background: var(--theme-body);
697 + padding: 3px 8px; border-radius: 4px;
698 + }
699 +
700 + /* Scroll indicator */
701 + .scroll-indicator {
702 + position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%);
703 + display: flex; flex-direction: column; align-items: center; gap: 8px;
704 + }
705 + .scroll-indicator span {
706 + font-family: 'JetBrains Mono', monospace; font-size: 9px;
707 + letter-spacing: 0.3em; color: var(--theme-texto);
708 + }
709 + .scroll-line {
710 + width: 1px; height: 40px;
711 + background: linear-gradient(180deg, var(--theme-texto), transparent);
712 + }
713 +
714 + /* Ticker */
715 + .ticker-wrapper {
716 + overflow: hidden; padding: 14px 0; background: var(--theme-surface);
717 + border-top: 1px solid var(--theme-borde); border-bottom: 1px solid var(--theme-borde);
718 + }
719 + .ticker-track {
720 + display: flex; gap: 32px; white-space: nowrap;
721 + animation: ticker 30s linear infinite; width: fit-content;
722 + }
723 + .ticker-item {
724 + font-family: 'JetBrains Mono', monospace; font-size: 11px;
725 + letter-spacing: 0.15em; color: var(--theme-texto);
726 + }
727 + .ticker-diamond { color: var(--theme-accent); }
728 +
729 + /* Reveal animation */
730 + .revealed { opacity: 1 !important; transform: translateY(0) !important; }
731 +
732 + /* Stories */
733 + .stories-section { padding: 100px 40px; max-width: 1200px; margin: 0 auto; }
734 + .stories-header {
735 + display: flex; justify-content: space-between; align-items: flex-end;
736 + margin-bottom: 56px;
737 + opacity: 0; transform: translateY(40px);
738 + transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1);
739 + }
740 + .stories-title-big {
741 + font-family: 'Archivo Condensed', system-ui, sans-serif;
742 + font-size: clamp(28px, 4vw, 48px); font-weight: 700;
743 + line-height: 1.1; letter-spacing: -0.01em;
744 + text-transform: uppercase;
745 + color: var(--theme-titulo);
746 + }
747 + .text-muted { color: var(--theme-texto); }
748 + .stories-link {
749 + font-family: 'DM Sans', sans-serif; font-size: 13px; color: var(--theme-accent);
750 + text-decoration: none; display: flex; align-items: center; gap: 6px;
751 + letter-spacing: 0.05em;
752 + }
753 + .stories-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--theme-borde); }
754 + .story-card {
755 + background: var(--theme-body); padding: 40px 32px; cursor: pointer;
756 + transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
757 + position: relative; min-height: 280px;
758 + display: flex; flex-direction: column; justify-content: space-between;
759 + opacity: 0; transform: translateY(40px);
760 + }
761 + .story-card.revealed { opacity: 1; transform: translateY(0); }
762 + .story-hovered { background: var(--theme-fill); }
763 + .story-accent-line {
764 + position: absolute; top: 0; left: 0; right: 0; height: 3px;
765 + transform-origin: left;
766 + transition: transform 0.4s cubic-bezier(0.16, 1, 0.3, 1);
767 + }
768 + .story-tag {
769 + font-family: 'JetBrains Mono', monospace; font-size: 10px; letter-spacing: 0.2em;
770 + }
771 + .story-card-title {
772 + font-size: 22px; font-weight: 700; line-height: 1.25;
773 + margin-top: 16px; letter-spacing: -0.01em;
774 + color: var(--theme-titulo);
775 + }
776 + .story-desc {
777 + font-family: 'DM Sans', sans-serif; font-size: 14px;
778 + color: var(--theme-texto); line-height: 1.6; margin-top: 12px;
779 + }
780 + .story-cta {
781 + font-family: 'DM Sans', sans-serif; font-size: 13px;
782 + color: var(--theme-texto); transition: color 0.3s; margin-top: 24px;
783 + display: flex; align-items: center; gap: 6px;
784 + }
785 + .story-cta-active { color: var(--theme-titulo); }
786 + .story-arrow { transition: transform 0.3s; display: inline-block; }
787 + .arrow-moved { transform: translateX(4px); }
788 +
789 + /* Responsive */
790 + @media (max-width: 1024px) {
791 + .stories-grid { grid-template-columns: repeat(2, 1fr); }
792 + }
793 +
794 + @media (max-width: 640px) {
795 + .page-header { padding: 16px 20px; }
796 + .mefp-logo { height: 52px; }
797 + .hero { padding: 100px 20px 40px; }
798 + .subtitle-stats { font-size: 11px; }
799 + .hero-title { font-size: clamp(32px, 8vw, 42px); }
800 + .mode-toggle-wrapper { margin-top: 40px; }
801 + .mode-toggle { flex-direction: column; gap: 0; }
802 + .toggle-btn:first-child { border-radius: 6px 6px 0 0; border-right: 1px solid var(--theme-borde); border-bottom: none; }
803 + .toggle-btn:last-child { border-radius: 0 0 6px 6px; }
804 + .toggle-divider { width: 100%; height: 1px; }
805 + .stories-section { padding-left: 20px; padding-right: 20px; }
806 + .stories-grid { grid-template-columns: 1fr; }
807 + .stories-header { flex-direction: column; align-items: flex-start; gap: 16px; }
808 + }
809 +</style>
......
1 <script> 1 <script>
2 const clasificadores = [ 2 const clasificadores = [
3 - { id: 'institucional', nombre: 'Institucional', descripcion: 'Ministerios, universidades, municipios, empresas públicas', disponible: true }, 3 + {
4 - { id: 'objeto-gasto', nombre: 'Objeto del Gasto', descripcion: 'Servicios personales, bienes, inversiones', disponible: true }, 4 + slug: "institucional",
5 - { id: 'rubros', nombre: 'Rubros de Ingresos', descripcion: 'Ingresos tributarios, no tributarios, transferencias', disponible: true }, 5 + ruta: "/clasificadores/institucional",
6 - { id: 'fuente', nombre: 'Fuente de Financiamiento', descripcion: 'Tesoro, créditos externos, recursos propios', disponible: false }, 6 + numero: "01",
7 - { id: 'organismo', nombre: 'Organismo Financiador', descripcion: 'BID, Banco Mundial, CAF', disponible: false }, 7 + nombre: "Institucional",
8 - { id: 'geografico', nombre: 'Geográfico', descripcion: 'Departamentos, provincias, municipios', disponible: false }, 8 + titulo: "Quién",
9 - { id: 'funcional', nombre: 'Funcional', descripcion: 'Educación, salud, defensa', disponible: false }, 9 + desc: "Las instituciones que gastan y administran recursos públicos",
10 - { id: 'economico', nombre: 'Económico', descripcion: 'Corrientes, de capital', disponible: false }, 10 + ejemplo: "Ministerios, municipios, YPFB…"
11 - { id: 'programatico', nombre: 'Programático', descripcion: 'Programas y proyectos', disponible: false }, 11 + },
12 + {
13 + slug: "objeto-gasto",
14 + ruta: "/clasificadores/objeto-gasto",
15 + numero: "02",
16 + nombre: "Objeto del Gasto",
17 + titulo: "En qué",
18 + desc: "La naturaleza del gasto: sueldos, obras, compras",
19 + ejemplo: "Servicios personales, bienes, inversión…"
20 + },
21 + {
22 + slug: "rubros",
23 + ruta: "/clasificadores/rubros",
24 + numero: "03",
25 + nombre: "Recursos por Rubros",
26 + titulo: "Cuánto ingresa",
27 + desc: "Los ingresos del Estado por tipo de recaudación",
28 + ejemplo: "Impuestos, regalías, tasas…"
29 + },
30 + {
31 + slug: "funcional",
32 + ruta: "/clasificadores/funcional",
33 + numero: "04",
34 + nombre: "Finalidad y Función",
35 + titulo: "Para qué",
36 + desc: "El propósito del gasto según funciones del Estado",
37 + ejemplo: "Salud, Educación, Defensa…"
38 + },
39 + {
40 + slug: "fuente",
41 + ruta: "/clasificadores/fuente",
42 + numero: "05",
43 + nombre: "Fuente de Financiamiento",
44 + titulo: "De dónde",
45 + desc: "El origen del dinero que financia cada gasto",
46 + ejemplo: "Tesoro, IDH, créditos externos…"
47 + },
48 + {
49 + slug: "organismo",
50 + ruta: "/clasificadores/organismo",
51 + numero: "06",
52 + nombre: "Organismo Financiador",
53 + titulo: "Quién financia",
54 + desc: "Los organismos detrás de los créditos y donaciones",
55 + ejemplo: "BID, CAF, Banco Mundial…"
56 + },
57 + {
58 + slug: "sectores",
59 + ruta: "/clasificadores/sectores",
60 + numero: "07",
61 + nombre: "Sector Económico",
62 + titulo: "Qué sector",
63 + desc: "Los sectores productivos que activa el gasto",
64 + ejemplo: "Hidrocarburos, agro, transporte…"
65 + },
12 ]; 66 ];
13 -</script> 67 +
68 + </script>
14 69
15 <svelte:head> 70 <svelte:head>
16 - <title>Clasificadores | Presupuesto Público</title> 71 + <title>Explorar los datos — Presupuesto Bolivia</title>
72 + <link rel="preconnect" href="https://fonts.googleapis.com" />
73 + <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" />
17 </svelte:head> 74 </svelte:head>
18 75
19 -<div class="max-w-4xl mx-auto p-4"> 76 +<main class="clasificadores-page">
20 - <p class="text-sm text-gray-500 mb-4"> 77 + <!-- Header de la página -->
21 - <a href="/">Inicio</a> / Clasificadores 78 + <header class="page-header">
22 - </p> 79 + <!-- Breadcrumb -->
23 - 80 + <nav class="breadcrumb">
24 - <h1 class="text-2xl mb-6">Clasificadores</h1> 81 + <a href="/" class="breadcrumb-link">← Inicio</a>
25 - 82 + </nav>
26 - <div class="space-y-2"> 83 + <div class="header-content">
27 - {#each clasificadores as item} 84 + <h1>Formas de leer el presupuesto</h1>
28 - {#if item.disponible} 85 + <p class="subtitulo">
29 - <a href="/clasificadores/{item.id}" class="block p-4 border rounded hover:bg-gray-50"> 86 + Cada ingreso y cada gasto del Estado se registra bajo distintas clasificaciones.
30 - <strong>{item.nombre}</strong> 87 + Aquí puedes explorar los datos según cada una de ellas.
31 - <p class="text-sm text-gray-600">{item.descripcion}</p> 88 + </p>
32 - </a> 89 + </div>
33 - {:else} 90 + </header>
34 - <div class="p-4 border rounded opacity-50"> 91 +
35 - <strong>{item.nombre}</strong> 92 + <!-- Grid de clasificadores -->
36 - <span class="text-xs text-gray-400 ml-2">(próximamente)</span> 93 + <section class="grid-clasificadores">
37 - <p class="text-sm text-gray-600">{item.descripcion}</p> 94 + {#each clasificadores as c}
38 - </div> 95 + <a href={c.ruta} class="card">
39 - {/if} 96 + <span class="card-meta">
97 + <span class="card-numero">{c.numero}</span>
98 + <span class="card-nombre">{c.nombre}</span>
99 + </span>
100 + <h2 class="card-titulo">{c.titulo}</h2>
101 + <p class="card-desc">{c.desc}</p>
102 + <p class="card-ejemplo">{c.ejemplo}</p>
103 + </a>
40 {/each} 104 {/each}
41 - </div> 105 + </section>
42 -</div> 106 +
107 + <!-- Footer contextual -->
108 + <footer class="page-footer">
109 + <p class="footnote">
110 + Fuente: SIGMA (Sistema Integrado de Gestión y Modernización Administrativa) y
111 + SIGEP (Sistema de Gestión Pública) — Ministerio de Economía y Finanzas Públicas.
112 + Datos desde 2005.
113 + </p>
114 + </footer>
115 +</main>
116 +
117 +<style>
118 + .clasificadores-page {
119 + background: var(--theme-body);
120 + color: var(--theme-titulo);
121 + font-family: 'Instrument Sans', sans-serif;
122 + min-height: 100vh;
123 + max-width: 1000px;
124 + margin: 0 auto;
125 + padding: 0 2rem;
126 + display: flex;
127 + flex-direction: column;
128 + justify-content: center;
129 + transition: background-color 0.2s, color 0.2s;
130 + }
131 +
132 + /* ── Header ── */
133 + .page-header {
134 + padding: 1.25rem 0 1.5rem;
135 + margin-bottom: 1.25rem;
136 + }
137 +
138 + .breadcrumb {
139 + margin-bottom: 0.75rem;
140 + }
141 +
142 + .breadcrumb-link {
143 + display: inline-block;
144 + color: var(--theme-texto);
145 + text-decoration: none;
146 + font-family: 'DM Mono', monospace;
147 + font-size: 0.75rem;
148 + letter-spacing: 0.05em;
149 + transition: color 0.2s;
150 + }
151 + .breadcrumb-link:hover {
152 + color: var(--theme-titulo);
153 + }
154 +
155 + .label-top {
156 + display: block;
157 + font-family: 'DM Mono', monospace;
158 + font-size: 0.68rem;
159 + letter-spacing: 0.15em;
160 + text-transform: uppercase;
161 + color: var(--theme-texto);
162 + margin-bottom: 0.5rem;
163 + }
164 +
165 + h1 {
166 + font-family: 'DM Serif Display', serif;
167 + font-size: clamp(1.8rem, 4vw, 2.5rem);
168 + line-height: 1.1;
169 + letter-spacing: -0.02em;
170 + margin-bottom: 0.75rem;
171 + color: var(--theme-titulo);
172 + }
173 +
174 + .subtitulo {
175 + max-width: 520px;
176 + font-size: 0.95rem;
177 + line-height: 1.5;
178 + color: var(--theme-texto);
179 + }
180 +
181 + /* ── Masonry Grid ── */
182 + .grid-clasificadores {
183 + column-count: 3;
184 + column-gap: 0.875rem;
185 + margin-bottom: 1.5rem;
186 + }
187 +
188 + .card {
189 + display: block;
190 + break-inside: avoid;
191 + margin-bottom: 0.875rem;
192 + padding: 1.25rem 1.35rem;
193 + background: var(--theme-surface);
194 + border-radius: 6px;
195 + text-decoration: none;
196 + color: var(--theme-titulo);
197 + transition: all 0.15s ease;
198 + box-shadow: 0 1px 2px rgba(0,0,0,0.04);
199 + border: 1px solid var(--theme-borde);
200 + }
201 +
202 + .card:hover {
203 + box-shadow: 0 2px 8px rgba(0,0,0,0.08);
204 + border-color: var(--theme-accent);
205 + }
206 +
207 + .card:nth-child(1),
208 + .card:nth-child(5) {
209 + padding: 1.5rem 1.35rem;
210 + }
211 +
212 + .card-meta {
213 + display: flex;
214 + align-items: center;
215 + gap: 0.4rem;
216 + margin-bottom: 0.6rem;
217 + }
218 +
219 + .card-numero {
220 + font-family: 'DM Mono', monospace;
221 + font-size: 0.7rem;
222 + font-weight: 500;
223 + color: var(--theme-accent);
224 + letter-spacing: 0.05em;
225 + }
226 +
227 + .card-nombre {
228 + font-family: 'DM Mono', monospace;
229 + font-size: 0.6rem;
230 + color: var(--theme-texto);
231 + letter-spacing: 0.02em;
232 + padding-left: 0.4rem;
233 + border-left: 1px solid var(--theme-borde);
234 + }
235 +
236 + .card-titulo {
237 + font-family: 'DM Serif Display', serif;
238 + font-size: 1.35rem;
239 + font-weight: 400;
240 + line-height: 1.15;
241 + letter-spacing: -0.015em;
242 + margin-bottom: 0.35rem;
243 + color: var(--theme-titulo);
244 + transition: color 0.15s;
245 + }
246 +
247 + .card:hover .card-titulo {
248 + color: var(--theme-accent);
249 + }
250 +
251 + .card-desc {
252 + font-size: 0.78rem;
253 + color: var(--theme-texto);
254 + line-height: 1.45;
255 + margin-bottom: 0.5rem;
256 + }
257 +
258 + .card-ejemplo {
259 + font-size: 0.68rem;
260 + color: var(--theme-texto);
261 + opacity: 0.7;
262 + line-height: 1.4;
263 + }
264 +
265 + /* ── Footer ── */
266 + .page-footer {
267 + padding-top: 1rem;
268 + display: flex;
269 + flex-direction: column;
270 + gap: 0.35rem;
271 + }
272 +
273 + .page-footer p {
274 + font-size: 0.75rem;
275 + color: var(--theme-texto);
276 + }
277 +
278 + .page-footer a {
279 + color: var(--theme-texto);
280 + text-decoration: none;
281 + border-bottom: 1px solid var(--theme-borde);
282 + transition: border-color 0.2s, color 0.2s;
283 + }
284 + .page-footer a:hover {
285 + border-color: var(--theme-accent);
286 + color: var(--theme-titulo);
287 + }
288 +
289 + .footnote {
290 + font-family: 'DM Mono', monospace;
291 + font-size: 0.58rem;
292 + color: var(--theme-texto);
293 + opacity: 0.7;
294 + line-height: 1.5;
295 + }
296 +
297 + @media (max-width: 900px) {
298 + .grid-clasificadores {
299 + column-count: 2;
300 + }
301 + .clasificadores-page {
302 + justify-content: flex-start;
303 + padding-top: 1rem;
304 + }
305 + }
306 +
307 + @media (max-width: 540px) {
308 + .clasificadores-page {
309 + padding: 1rem 1rem 1.5rem;
310 + justify-content: flex-start;
311 + }
312 + .grid-clasificadores {
313 + column-count: 1;
314 + }
315 + .card {
316 + padding: 1rem 1.15rem;
317 + }
318 + .card:nth-child(1),
319 + .card:nth-child(5) {
320 + padding: 1rem 1.15rem;
321 + }
322 + .card-pregunta {
323 + font-size: 1.1rem;
324 + }
325 + h1 { font-size: 1.4rem; }
326 + }
327 +</style>
......
1 <script> 1 <script>
2 import { supabase } from '$lib/supabase'; 2 import { supabase } from '$lib/supabase';
3 import { onMount } from 'svelte'; 3 import { onMount } from 'svelte';
4 + import { page } from '$app/stores';
5 + import { goto } from '$app/navigation';
4 6
5 let loading = $state(true); 7 let loading = $state(true);
6 let searchQuery = $state(''); 8 let searchQuery = $state('');
...@@ -8,6 +10,22 @@ ...@@ -8,6 +10,22 @@
8 let allEntidades = $state([]); 10 let allEntidades = $state([]);
9 let selectedArea = $state(null); 11 let selectedArea = $state(null);
10 let highlightedEntidad = $state(null); 12 let highlightedEntidad = $state(null);
13 + let sidebarOpen = $state(false);
14 +
15 + // Modo de visualización desde URL
16 + let viewMode = $derived($page.url.searchParams.get('modo') || 'lista');
17 +
18 + // Función para cambiar de modo preservando otros parámetros
19 + function setMode(mode) {
20 + const params = new URLSearchParams($page.url.searchParams);
21 + if (mode === 'lista') {
22 + params.delete('modo');
23 + } else {
24 + params.set('modo', mode);
25 + }
26 + const query = params.toString();
27 + goto(`?${query}`, { replaceState: true, noScroll: true });
28 + }
11 29
12 onMount(async () => { 30 onMount(async () => {
13 const { data, error } = await supabase 31 const { data, error } = await supabase
...@@ -95,6 +113,7 @@ ...@@ -95,6 +113,7 @@
95 selectedArea = area; 113 selectedArea = area;
96 searchQuery = ''; 114 searchQuery = '';
97 highlightedEntidad = null; 115 highlightedEntidad = null;
116 + sidebarOpen = false;
98 } 117 }
99 118
100 function goToSearchResult(item) { 119 function goToSearchResult(item) {
...@@ -104,6 +123,7 @@ ...@@ -104,6 +123,7 @@
104 selectedArea = targetArea; 123 selectedArea = targetArea;
105 highlightedEntidad = item.entidad; 124 highlightedEntidad = item.entidad;
106 searchQuery = ''; 125 searchQuery = '';
126 + sidebarOpen = false;
107 127
108 setTimeout(() => { 128 setTimeout(() => {
109 const element = document.getElementById(`ent-${item.entidad}`); 129 const element = document.getElementById(`ent-${item.entidad}`);
...@@ -144,27 +164,117 @@ ...@@ -144,27 +164,117 @@
144 let searchResults = $derived(searchGlobal(searchQuery)); 164 let searchResults = $derived(searchGlobal(searchQuery));
145 let isSearching = $derived(searchQuery.length >= 2); 165 let isSearching = $derived(searchQuery.length >= 2);
146 let totalEntidades = $derived(allEntidades.length); 166 let totalEntidades = $derived(allEntidades.length);
167 + let totalAreas = $derived(areas.length);
147 </script> 168 </script>
148 169
149 <svelte:head> 170 <svelte:head>
150 <title>Institucional | Presupuesto Público</title> 171 <title>Institucional | Presupuesto Público</title>
172 + <link rel="preconnect" href="https://fonts.googleapis.com" />
173 + <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" />
151 </svelte:head> 174 </svelte:head>
152 175
153 -<div class="min-h-screen bg-white"> 176 +<div class="min-h-screen" style="font-family: 'Instrument Sans', sans-serif; background-color: var(--theme-body); color: var(--theme-titulo); transition: background-color 0.2s, color 0.2s;">
154 - <!-- Header --> 177 + <!-- Header pedagógico -->
155 - <header class="border-b bg-slate-50"> 178 + <header class="border-b" style="border-color: var(--theme-borde); background-color: var(--theme-body);">
156 - <div class="max-w-screen-2xl mx-auto px-6 py-4"> 179 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-6">
157 - <nav class="text-sm text-slate-500 mb-2"> 180 + <!-- Breadcrumb: responsive -->
158 - <a href="/" class="hover:text-slate-700">Inicio</a> 181 + <nav class="mb-4" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;">
159 - <span class="mx-2">/</span> 182 + <!-- Móvil: solo padre -->
160 - <a href="/clasificadores" class="hover:text-slate-700">Clasificadores</a> 183 + <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);">
161 - <span class="mx-2">/</span> 184 + ← Clasificadores
162 - <span class="text-slate-900">Institucional</span> 185 + </a>
186 + <!-- Desktop: ruta completa -->
187 + <div class="hidden sm:flex items-center gap-2" style="color: var(--theme-texto);">
188 + <a href="/" class="transition-colors hover:opacity-80">Inicio</a>
189 + <span style="opacity: 0.5;">/</span>
190 + <a href="/clasificadores" class="transition-colors hover:opacity-80">Clasificadores</a>
191 + </div>
163 </nav> 192 </nav>
164 - <div class="flex items-center justify-between"> 193 +
165 - <div> 194 + <div class="max-w-3xl">
166 - <h1 class="text-2xl font-light text-slate-900">Clasificador Institucional</h1> 195 + <p class="text-xs uppercase tracking-widest mb-2" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
167 - <p class="text-sm text-slate-500 mt-1">{totalEntidades || '...'} entidades públicas</p> 196 + Clasificador 01
197 + </p>
198 + <h1 class="text-3xl mb-3" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);">
199 + ¿Quién gasta?
200 + </h1>
201 + <p class="leading-relaxed mb-3" style="color: var(--theme-texto);">
202 + Este clasificador identifica a las <strong style="color: var(--theme-titulo);">instituciones del Estado</strong>
203 + que administran y ejecutan el presupuesto público: ministerios, empresas estatales,
204 + gobiernos locales y entidades descentralizadas.
205 + </p>
206 +
207 + {#if totalEntidades > 0}
208 + <p class="text-sm mb-5" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
209 + <span style="color: var(--theme-titulo); font-weight: 500;">{totalEntidades}</span> entidades públicas en
210 + <span style="color: var(--theme-titulo); font-weight: 500;">{totalAreas}</span> áreas institucionales
211 + </p>
212 + {/if}
213 +
214 + <!-- Jerarquía -->
215 + <div class="hidden sm:flex flex-wrap items-center gap-2 sm:gap-3 text-xs mb-6" style="font-family: 'DM Mono', monospace;">
216 + <span class="px-2.5 py-1 rounded-full font-medium" style="background-color: var(--theme-accent); color: var(--theme-body); opacity: 0.9;">
217 + Área
218 + </span>
219 + <span style="color: var(--theme-texto);">→</span>
220 + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">
221 + Subárea
222 + </span>
223 + <span style="color: var(--theme-texto);">→</span>
224 + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">
225 + Entidad
226 + </span>
227 + </div>
228 + <!-- Versión móvil simplificada -->
229 + <p class="sm:hidden text-sm mb-4" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
230 + Jerarquía: Área → Subárea → Entidad
231 + </p>
232 + </div>
233 +
234 + <!-- Controles de visualización -->
235 + <div class="mt-8">
236 + <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
237 + <!-- Toggle Vista -->
238 + <div class="flex items-center gap-3">
239 + <span class="text-sm font-medium" style="color: var(--theme-titulo);">Explorar como</span>
240 + <nav class="inline-flex rounded-xl border-2 p-1 shadow-sm" style="border-color: var(--theme-titulo); background-color: var(--theme-surface);">
241 + <a
242 + href="?modo=lista"
243 + class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
244 + style="{viewMode === 'lista' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
245 + onclick={(e) => { e.preventDefault(); setMode('lista'); }}
246 + >
247 + Lista
248 + </a>
249 + <a
250 + href="?modo=mapa"
251 + class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
252 + style="{viewMode === 'mapa' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
253 + onclick={(e) => { e.preventDefault(); setMode('mapa'); }}
254 + >
255 + Mapa
256 + </a>
257 + <a
258 + href="?modo=comparar"
259 + class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
260 + style="{viewMode === 'comparar' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
261 + onclick={(e) => { e.preventDefault(); setMode('comparar'); }}
262 + >
263 + Comparar
264 + </a>
265 + </nav>
266 + </div>
267 +
268 + <!-- Guía inline -->
269 + <div class="hidden sm:block text-sm border-l pl-4" style="color: var(--theme-texto); border-color: var(--theme-borde);">
270 + {#if viewMode === 'lista'}
271 + <span>Navega por áreas y subáreas</span>
272 + {:else if viewMode === 'mapa'}
273 + <span>Visualización geográfica del presupuesto</span>
274 + {:else}
275 + <span>Compara dos entidades o periodos</span>
276 + {/if}
277 + </div>
168 </div> 278 </div>
169 </div> 279 </div>
170 </div> 280 </div>
...@@ -172,64 +282,113 @@ ...@@ -172,64 +282,113 @@
172 282
173 {#if loading} 283 {#if loading}
174 <div class="flex items-center justify-center py-20"> 284 <div class="flex items-center justify-center py-20">
175 - <p class="text-slate-500">Cargando clasificador...</p> 285 + <p style="color: var(--theme-texto);">Cargando clasificador...</p>
176 </div> 286 </div>
177 {:else} 287 {:else}
178 - <div class="max-w-screen-2xl mx-auto flex"> 288 + {#if viewMode === 'lista'}
289 + <!-- ═══════════════════════════════════════════════════════════ -->
290 + <!-- MODO LISTA: Navegación jerárquica de entidades -->
291 + <!-- ═══════════════════════════════════════════════════════════ -->
292 +
293 + <!-- Botón móvil para abrir sidebar -->
294 + <div class="lg:hidden px-4 py-3 border-b" style="border-color: var(--theme-borde); background-color: var(--theme-fill);">
295 + <button
296 + onclick={() => sidebarOpen = !sidebarOpen}
297 + class="flex items-center gap-2 text-sm"
298 + style="color: var(--theme-texto);"
299 + >
300 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
301 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
302 + </svg>
303 + <span class="font-medium">{selectedArea?.nombre || 'Seleccionar área'}</span>
304 + <svg class="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
305 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
306 + </svg>
307 + </button>
308 + </div>
309 +
310 + <div class="max-w-screen-xl mx-auto flex px-4">
179 <!-- Sidebar izquierda: Áreas --> 311 <!-- Sidebar izquierda: Áreas -->
180 - <aside class="w-64 flex-shrink-0 border-r bg-slate-50/50"> 312 + <!-- En móvil: overlay, en desktop: sidebar fijo -->
181 - <div class="sticky top-0 h-screen overflow-y-auto p-4"> 313 + {#if sidebarOpen}
314 + <div class="fixed inset-0 bg-black/30 z-40 lg:hidden" onclick={() => sidebarOpen = false}></div>
315 + {/if}
316 + <aside class="
317 + {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
318 + lg:translate-x-0
319 + fixed lg:relative
320 + inset-y-0 left-0
321 + w-72 lg:w-64
322 + z-50 lg:z-auto
323 + transition-transform duration-200 ease-in-out
324 + lg:flex-shrink-0
325 + shadow-xl lg:shadow-none
326 + sidebar-left
327 + ">
328 + <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-6">
329 + <!-- Cerrar en móvil -->
330 + <div class="flex justify-between items-center mb-4 lg:hidden">
331 + <span class="text-sm font-medium" style="color: var(--theme-titulo);">Áreas institucionales</span>
332 + <button onclick={() => sidebarOpen = false} style="color: var(--theme-texto);">
333 + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
334 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
335 + </svg>
336 + </button>
337 + </div>
338 +
182 <!-- Buscador --> 339 <!-- Buscador -->
183 <div class="mb-6"> 340 <div class="mb-6">
184 <input 341 <input
185 type="text" 342 type="text"
186 bind:value={searchQuery} 343 bind:value={searchQuery}
187 placeholder="Buscar entidad..." 344 placeholder="Buscar entidad..."
188 - class="w-full px-3 py-2 text-sm border border-slate-200 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" 345 + class="w-full px-3 py-2 text-sm rounded-md focus:outline-none focus:ring-2 focus:ring-offset-0"
346 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
189 /> 347 />
190 </div> 348 </div>
191 349
192 <!-- Resultados de búsqueda --> 350 <!-- Resultados de búsqueda -->
193 {#if isSearching} 351 {#if isSearching}
194 <div class="mb-4"> 352 <div class="mb-4">
195 - <p class="text-xs text-slate-500 uppercase tracking-wide mb-2"> 353 + <p class="text-xs uppercase tracking-wide mb-2" style="color: var(--theme-texto);">
196 {searchResults.length} resultados 354 {searchResults.length} resultados
197 </p> 355 </p>
198 <div class="space-y-1"> 356 <div class="space-y-1">
199 {#each searchResults as result} 357 {#each searchResults as result}
200 <button 358 <button
201 - class="w-full text-left px-2 py-2 text-sm rounded hover:bg-white hover:shadow-sm transition-all" 359 + class="w-full text-left px-2 py-2 text-sm rounded transition-all"
360 + style="color: var(--theme-titulo);"
202 onclick={() => goToSearchResult(result)} 361 onclick={() => goToSearchResult(result)}
203 > 362 >
204 - <span class="text-xs text-slate-400 block">{result.desc_area}</span> 363 + <span class="text-xs block" style="color: var(--theme-texto);">{result.desc_area}</span>
205 - <span class="text-slate-700">{result.desc_entidad}</span> 364 + <span>{result.desc_entidad}</span>
206 {#if result.sigla_entidad} 365 {#if result.sigla_entidad}
207 - <span class="text-slate-400 text-xs ml-1">({result.sigla_entidad})</span> 366 + <span class="text-xs ml-1" style="color: var(--theme-texto); opacity: 0.7;">({result.sigla_entidad})</span>
208 {/if} 367 {/if}
209 </button> 368 </button>
210 {/each} 369 {/each}
211 {#if searchResults.length === 0} 370 {#if searchResults.length === 0}
212 - <p class="text-sm text-slate-400 px-2">Sin resultados</p> 371 + <p class="text-sm px-2" style="color: var(--theme-texto);">Sin resultados</p>
213 {/if} 372 {/if}
214 </div> 373 </div>
215 </div> 374 </div>
216 {:else} 375 {:else}
217 <!-- Lista de áreas --> 376 <!-- Lista de áreas -->
218 <nav> 377 <nav>
219 - <p class="text-xs text-slate-500 uppercase tracking-wide mb-3 px-2">Áreas</p> 378 + <p class="text-xs uppercase tracking-wide mb-3 px-2 hidden lg:block" style="color: var(--theme-texto);">Áreas</p>
220 <ul class="space-y-1"> 379 <ul class="space-y-1">
221 {#each areas as area} 380 {#each areas as area}
222 <li> 381 <li>
223 <button 382 <button
224 - class="w-full text-left px-3 py-2 rounded-md text-sm transition-all 383 + class="w-full text-left px-3 py-2 rounded-md text-sm transition-all"
225 - {selectedArea?.codigo === area.codigo 384 + style="{selectedArea?.codigo === area.codigo
226 - ? 'bg-blue-50 text-blue-900 font-medium border-l-2 border-blue-500' 385 + ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent); color: var(--theme-titulo); font-weight: 500; border-left: 2px solid var(--theme-accent);`
227 - : 'text-slate-600 hover:bg-white hover:text-slate-900'}" 386 + : `color: var(--theme-texto);`}"
228 onclick={() => selectArea(area)} 387 onclick={() => selectArea(area)}
229 > 388 >
230 <span class="flex justify-between items-center"> 389 <span class="flex justify-between items-center">
231 <span class="truncate">{area.nombre}</span> 390 <span class="truncate">{area.nombre}</span>
232 - <span class="text-xs text-slate-400 ml-2 flex-shrink-0">{area.entidades.length}</span> 391 + <span class="text-xs ml-2 flex-shrink-0" style="color: var(--theme-texto); opacity: 0.7;">{area.entidades.length}</span>
233 </span> 392 </span>
234 </button> 393 </button>
235 </li> 394 </li>
...@@ -241,53 +400,61 @@ ...@@ -241,53 +400,61 @@
241 </aside> 400 </aside>
242 401
243 <!-- Contenido principal --> 402 <!-- Contenido principal -->
244 - <main class="flex-1 min-w-0"> 403 + <main class="flex-1 min-w-0 lg:border-l" style="border-color: var(--theme-borde);">
245 - <div class="px-8 py-6"> 404 + <div class="px-4 sm:px-6 lg:px-10 py-6 lg:py-8">
246 {#if selectedArea} 405 {#if selectedArea}
247 <!-- Título del área --> 406 <!-- Título del área -->
248 - <div class="mb-8 pb-6 border-b"> 407 + <div class="mb-8 lg:mb-10 pb-6 lg:pb-8 border-b" style="border-color: var(--theme-borde);">
249 - <h2 class="text-xl font-medium text-slate-900 mb-1">{selectedArea.nombre}</h2> 408 + <p class="text-sm mb-1" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">Área {selectedArea.codigo}</p>
250 - <p class="text-sm text-slate-500">{selectedArea.entidades.length} entidades</p> 409 + <h2 class="text-lg sm:text-xl font-medium mb-2 flex items-center gap-2 sm:gap-3 flex-wrap" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);">
410 + {selectedArea.nombre}
411 + </h2>
412 + <p class="text-sm" style="color: var(--theme-texto);">{selectedArea.entidades.length} entidades en esta área</p>
251 </div> 413 </div>
252 414
253 <!-- Subáreas y entidades --> 415 <!-- Subáreas y entidades -->
254 - <div class="space-y-8"> 416 + <div class="space-y-10">
255 {#each areaSubareas as subarea} 417 {#each areaSubareas as subarea}
256 <section id="subarea-{subarea.nombre.replace(/\s+/g, '-')}" class="scroll-mt-4"> 418 <section id="subarea-{subarea.nombre.replace(/\s+/g, '-')}" class="scroll-mt-4">
257 {#if areaSubareas.length > 1 || subarea.nombre !== selectedArea.nombre} 419 {#if areaSubareas.length > 1 || subarea.nombre !== selectedArea.nombre}
258 - <div class="flex items-start gap-4 mb-4"> 420 + <div class="flex items-start gap-2 sm:gap-4 mb-4">
259 <div class="flex-1"> 421 <div class="flex-1">
260 - <h3 class="text-lg font-medium text-slate-900">{subarea.nombre}</h3> 422 + <h3 class="text-base sm:text-lg font-medium" style="color: var(--theme-titulo);">{subarea.nombre}</h3>
261 - <p class="text-sm text-slate-500">{subarea.entidades.length} entidades</p> 423 + <p class="text-sm" style="color: var(--theme-texto);">{subarea.entidades.length} entidades</p>
262 </div> 424 </div>
263 </div> 425 </div>
264 {/if} 426 {/if}
265 427
266 <!-- Entidades --> 428 <!-- Entidades -->
267 - <div class="border rounded-lg divide-y"> 429 + <div class="ml-0 sm:ml-4 lg:ml-8 space-y-2 border-l-2 pl-4 sm:pl-6 lg:pl-8" style="border-color: var(--theme-borde);">
268 {#each subarea.entidades as entidad} 430 {#each subarea.entidades as entidad}
269 <div 431 <div
270 id="ent-{entidad.codigo}" 432 id="ent-{entidad.codigo}"
271 - class="scroll-mt-4 {highlightedEntidad === entidad.codigo ? 'ring-2 ring-blue-200' : ''}" 433 + class="scroll-mt-4"
434 + style="{highlightedEntidad === entidad.codigo ? `box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); border-radius: 0.5rem;` : ''}"
272 > 435 >
273 <a 436 <a
274 href="/entidad/{entidad.codigo}" 437 href="/entidad/{entidad.codigo}"
275 - class="flex items-center justify-between px-4 py-3 hover:bg-slate-50 transition-colors" 438 + class="block px-4 py-3 rounded-lg border transition-all group"
439 + style="background-color: var(--theme-surface); border-color: var(--theme-borde);"
276 > 440 >
277 - <div class="flex-1 min-w-0"> 441 + <div class="flex items-start justify-between gap-3">
278 - <div class="flex items-center gap-2"> 442 + <div class="flex-1 min-w-0">
279 - <span class="text-slate-900">{entidad.nombre}</span> 443 + <div class="flex items-center gap-2 flex-wrap">
280 - {#if entidad.sigla} 444 + <span class="font-mono text-xs" style="color: var(--theme-texto);">{entidad.codigo}</span>
281 - <span class="text-sm text-slate-400">({entidad.sigla})</span> 445 + <span class="font-medium" style="color: var(--theme-titulo);">{entidad.nombre}</span>
282 - {/if} 446 + {#if entidad.sigla}
447 + <span class="text-sm" style="color: var(--theme-texto); opacity: 0.7;">({entidad.sigla})</span>
448 + {/if}
449 + </div>
450 + <p class="text-xs mt-1" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
451 + {formatGestiones(entidad.gestiones)}
452 + </p>
283 </div> 453 </div>
284 - <p class="text-xs text-slate-400 mt-0.5"> 454 + <svg class="w-4 h-4 flex-shrink-0 mt-1 transition-colors group-hover:text-[var(--theme-accent)]" style="color: var(--theme-texto); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
285 - Código: {entidad.codigo} · {formatGestiones(entidad.gestiones)} 455 + <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" />
286 - </p> 456 + </svg>
287 </div> 457 </div>
288 - <svg class="w-4 h-4 text-slate-300 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
289 - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
290 - </svg>
291 </a> 458 </a>
292 </div> 459 </div>
293 {/each} 460 {/each}
...@@ -301,29 +468,147 @@ ...@@ -301,29 +468,147 @@
301 468
302 <!-- Sidebar derecha: En esta página --> 469 <!-- Sidebar derecha: En esta página -->
303 <aside class="w-56 flex-shrink-0 hidden xl:block"> 470 <aside class="w-56 flex-shrink-0 hidden xl:block">
304 - <div class="sticky top-0 h-screen overflow-y-auto p-4 border-l"> 471 + <div class="sticky top-0 h-screen overflow-y-auto py-8 pl-6 border-l" style="border-color: var(--theme-borde);">
305 - <p class="text-xs text-slate-500 uppercase tracking-wide mb-3">En esta página</p> 472 + <p class="text-xs uppercase tracking-wide mb-3" style="color: var(--theme-texto);">En esta página</p>
306 {#if selectedArea && !isSearching} 473 {#if selectedArea && !isSearching}
307 - <nav class="space-y-2"> 474 + <nav class="space-y-3">
308 {#each areaSubareas as subarea} 475 {#each areaSubareas as subarea}
309 {#if areaSubareas.length > 1 || subarea.nombre !== selectedArea.nombre} 476 {#if areaSubareas.length > 1 || subarea.nombre !== selectedArea.nombre}
310 <a 477 <a
311 href="#subarea-{subarea.nombre.replace(/\s+/g, '-')}" 478 href="#subarea-{subarea.nombre.replace(/\s+/g, '-')}"
312 - class="block text-sm text-slate-600 hover:text-blue-600 truncate" 479 + class="block text-sm leading-snug transition-colors hover:opacity-80"
480 + style="color: var(--theme-titulo);"
313 title={subarea.nombre} 481 title={subarea.nombre}
314 > 482 >
315 {subarea.nombre} 483 {subarea.nombre}
316 - <span class="text-xs text-slate-400 ml-1">({subarea.entidades.length})</span> 484 + <span class="text-xs ml-1" style="color: var(--theme-texto);">({subarea.entidades.length})</span>
317 </a> 485 </a>
318 {/if} 486 {/if}
319 {/each} 487 {/each}
320 {#if areaSubareas.length === 1 && areaSubareas[0].nombre === selectedArea.nombre} 488 {#if areaSubareas.length === 1 && areaSubareas[0].nombre === selectedArea.nombre}
321 - <p class="text-sm text-slate-400">{selectedArea.entidades.length} entidades</p> 489 + <p class="text-sm" style="color: var(--theme-texto);">{selectedArea.entidades.length} entidades</p>
322 {/if} 490 {/if}
323 </nav> 491 </nav>
324 {/if} 492 {/if}
325 </div> 493 </div>
326 </aside> 494 </aside>
327 </div> 495 </div>
496 +
497 + {:else if viewMode === 'mapa'}
498 + <!-- ═══════════════════════════════════════════════════════════ -->
499 + <!-- MODO MAPA: Visualización geográfica del presupuesto -->
500 + <!-- ═══════════════════════════════════════════════════════════ -->
501 +
502 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-8">
503 + <div class="border-2 border-dashed rounded-2xl min-h-[500px] flex flex-col items-center justify-center p-8" style="background-color: var(--theme-fill); border-color: var(--theme-borde);">
504 + <svg class="w-16 h-16 mb-4" style="color: var(--theme-texto); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
505 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
506 + </svg>
507 + <h3 class="text-lg font-medium mb-2" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);">
508 + Mapa de Bolivia
509 + </h3>
510 + <p class="text-center max-w-md mb-4" style="color: var(--theme-texto);">
511 + Visualización geográfica del presupuesto por departamento y municipio.
512 + El color indica el monto ejecutado por cada entidad territorial.
513 + </p>
514 + <div class="flex flex-wrap gap-2 justify-center text-xs" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
515 + <span class="px-2 py-1 rounded border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">9 departamentos</span>
516 + <span class="px-2 py-1 rounded border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">340 municipios</span>
517 + <span class="px-2 py-1 rounded border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">TopoJSON + D3.js</span>
518 + </div>
519 + </div>
520 +
521 + <div class="mt-6 p-4 rounded-lg border" style="background-color: color-mix(in srgb, var(--theme-accent) 10%, transparent); border-color: color-mix(in srgb, var(--theme-accent) 30%, transparent);">
522 + <div class="flex items-start gap-3">
523 + <svg class="w-5 h-5 shrink-0 mt-0.5" style="color: var(--theme-accent);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
524 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
525 + </svg>
526 + <div>
527 + <p class="text-sm font-medium" style="color: var(--theme-titulo);">Próximamente</p>
528 + <p class="text-sm mt-1" style="color: var(--theme-texto);">
529 + Esta visualización mostrará el presupuesto ejecutado en cada territorio de Bolivia.
530 + Podrás explorar por departamento, provincia y municipio.
531 + </p>
532 + </div>
533 + </div>
534 + </div>
535 + </div>
536 +
537 + {:else if viewMode === 'comparar'}
538 + <!-- ═══════════════════════════════════════════════════════════ -->
539 + <!-- MODO COMPARAR: Dos entidades lado a lado -->
540 + <!-- ═══════════════════════════════════════════════════════════ -->
541 +
542 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-8">
543 + <div class="flex flex-col lg:flex-row gap-6 lg:gap-8 mb-8">
544 + <!-- Lado A -->
545 + <div class="flex-1">
546 + <div class="flex items-center gap-3 mb-4">
547 + <span class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">A</span>
548 + <span class="font-medium" style="color: var(--theme-titulo);">Selecciona una entidad</span>
549 + </div>
550 + <div class="border-2 border-dashed rounded-xl min-h-[350px] flex flex-col items-center justify-center p-6" style="background-color: color-mix(in srgb, var(--theme-accent) 5%, transparent); border-color: color-mix(in srgb, var(--theme-accent) 30%, transparent);">
551 + <svg class="w-12 h-12 mb-3" style="color: var(--theme-accent); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
552 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
553 + </svg>
554 + <p class="font-medium" style="color: var(--theme-accent);">Entidad A</p>
555 + <p class="text-sm mt-1" style="color: var(--theme-texto);">Evolución presupuestaria</p>
556 + </div>
557 + </div>
558 +
559 + <!-- Separador -->
560 + <div class="flex lg:flex-col items-center justify-center gap-2">
561 + <span class="font-medium text-sm" style="color: var(--theme-texto);">vs</span>
562 + </div>
563 +
564 + <!-- Lado B -->
565 + <div class="flex-1">
566 + <div class="flex items-center gap-3 mb-4">
567 + <span class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm" style="background-color: color-mix(in srgb, #8b5cf6 20%, transparent); color: #8b5cf6;">B</span>
568 + <span class="font-medium" style="color: var(--theme-titulo);">Selecciona otra entidad</span>
569 + </div>
570 + <div class="border-2 border-dashed rounded-xl min-h-[350px] flex flex-col items-center justify-center p-6" style="background-color: color-mix(in srgb, #8b5cf6 5%, transparent); border-color: color-mix(in srgb, #8b5cf6 30%, transparent);">
571 + <svg class="w-12 h-12 mb-3" style="color: #8b5cf6; opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
572 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
573 + </svg>
574 + <p class="font-medium" style="color: #8b5cf6;">Entidad B</p>
575 + <p class="text-sm mt-1" style="color: var(--theme-texto);">Evolución presupuestaria</p>
576 + </div>
577 + </div>
578 + </div>
579 +
580 + <div class="border rounded-xl p-6" style="background-color: var(--theme-fill); border-color: var(--theme-borde);">
581 + <h3 class="text-sm font-medium uppercase tracking-wide mb-4" style="font-family: 'DM Mono', monospace; color: var(--theme-titulo);">
582 + Comparación
583 + </h3>
584 + <div class="grid sm:grid-cols-3 gap-4 text-center">
585 + <div class="p-4 rounded-lg border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">
586 + <p class="text-xs uppercase tracking-wide mb-1" style="color: var(--theme-texto);">Presupuesto total</p>
587 + <p class="text-sm italic" style="color: var(--theme-texto);">Selecciona entidades</p>
588 + </div>
589 + <div class="p-4 rounded-lg border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">
590 + <p class="text-xs uppercase tracking-wide mb-1" style="color: var(--theme-texto);">Diferencia</p>
591 + <p class="text-sm italic" style="color: var(--theme-texto);">Selecciona entidades</p>
592 + </div>
593 + <div class="p-4 rounded-lg border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">
594 + <p class="text-xs uppercase tracking-wide mb-1" style="color: var(--theme-texto);">Tendencia</p>
595 + <p class="text-sm italic" style="color: var(--theme-texto);">Selecciona entidades</p>
596 + </div>
597 + </div>
598 + </div>
599 + </div>
600 + {/if}
328 {/if} 601 {/if}
329 </div> 602 </div>
603 +
604 +<style>
605 + /* Sidebar izquierdo: con fondo en móvil, transparente en desktop */
606 + .sidebar-left {
607 + background-color: var(--theme-surface);
608 + }
609 + @media (min-width: 1024px) {
610 + .sidebar-left {
611 + background-color: transparent;
612 + }
613 + }
614 +</style>
......
1 <script> 1 <script>
2 import { supabase } from '$lib/supabase'; 2 import { supabase } from '$lib/supabase';
3 import { onMount } from 'svelte'; 3 import { onMount } from 'svelte';
4 + import { page } from '$app/stores';
5 + import { goto } from '$app/navigation';
6 + import * as d3 from 'd3';
4 7
5 let loading = $state(true); 8 let loading = $state(true);
6 let searchQuery = $state(''); 9 let searchQuery = $state('');
...@@ -9,8 +12,506 @@ ...@@ -9,8 +12,506 @@
9 let selectedGrupo = $state(null); 12 let selectedGrupo = $state(null);
10 let selectedItem = $state(null); 13 let selectedItem = $state(null);
11 let highlightedItem = $state(null); 14 let highlightedItem = $state(null);
15 + let sidebarOpen = $state(false);
16 +
17 + // Modo de visualización desde URL
18 + let viewMode = $derived($page.url.searchParams.get('modo') || 'lista');
19 +
20 + // Función para cambiar de modo preservando otros parámetros
21 + function setMode(mode) {
22 + const params = new URLSearchParams($page.url.searchParams);
23 + if (mode === 'lista') {
24 + params.delete('modo');
25 + } else {
26 + params.set('modo', mode);
27 + }
28 + const query = params.toString();
29 + goto(`?${query}`, { replaceState: true, noScroll: true });
30 + }
31 + let availableYears = $state([]);
32 + let selectedYear = $state(null);
33 + let entities = $state([]);
34 + let selectedEntity = $state(null); // null = Todo el Estado
35 + let entitySearchQuery = $state('');
36 + let entityDropdownOpen = $state(false);
37 +
38 + // Estado para modo comparar
39 + let compareA = $state({ year: null, entity: null });
40 + let compareB = $state({ year: null, entity: null });
41 + let compareADropdownOpen = $state(false);
42 + let compareBDropdownOpen = $state(false);
43 + let compareASearchQuery = $state('');
44 + let compareBSearchQuery = $state('');
45 +
46 + // ══════════════════════════════════════════════════════════════
47 + // ESTADO PARA TREEMAP
48 + // ══════════════════════════════════════════════════════════════
49 + let treemapData = $state([]);
50 + let treemapRoot = $state(null);
51 + let treemapNodes = $state([]);
52 + let currentTreemapNode = $state(null); // Nodo actual para drill-down
53 + let treemapBreadcrumb = $state([]); // Path de navegación
54 + let treemapWidth = $state(0);
55 + let treemapHeight = $state(500);
56 + let treemapContainer = $state(null);
57 + let hoveredNode = $state(null);
58 +
59 + // Selector de nivel para vista aplanada
60 + let treemapViewLevel = $state('partida'); // 'jerarquico' | 'subgrupo' | 'partida' | 'subpartida'
61 + const NIVEL_OPTIONS = [
62 + { value: 'jerarquico', label: 'Jerarquía/Grupos', desc: 'Navegación drill-down' },
63 + { value: 'subgrupo', label: 'Subgrupos', desc: '~30 categorías' },
64 + { value: 'partida', label: 'Partidas', desc: '~100 categorías' },
65 + { value: 'subpartida', label: 'Subpartidas', desc: 'Máximo detalle' }
66 + ];
67 +
68 + // Colores por grupo (primer dígito del código)
69 + const GRUPO_COLORS = {
70 + '1': '#4E79A7', // Servicios Personales - azul
71 + '2': '#F28E2B', // Servicios No Personales - naranja
72 + '3': '#E15759', // Materiales y Suministros - rojo
73 + '4': '#76B7B2', // Activos Reales - teal
74 + '5': '#59A14F', // Activos Financieros - verde
75 + '6': '#EDC948', // Servicio de la Deuda - amarillo
76 + '7': '#B07AA1', // Transferencias - púrpura
77 + '8': '#FF9DA7', // Impuestos, Regalías - rosa
78 + '9': '#9C755F', // Otros Gastos - marrón
79 + };
80 +
81 + // Función para obtener color según el grupo
82 + function getNodeColor(objeto, opacity = 1) {
83 + const grupoDigit = String(objeto).charAt(0);
84 + const baseColor = GRUPO_COLORS[grupoDigit] || '#888888';
85 + if (opacity === 1) return baseColor;
86 + return d3.color(baseColor).copy({opacity}).formatRgb();
87 + }
88 +
89 + // Función para determinar si el texto debe ser claro u oscuro según el fondo
90 + function getTextColor(objeto) {
91 + const grupoDigit = String(objeto).charAt(0);
92 + const baseColor = GRUPO_COLORS[grupoDigit] || '#888888';
93 + const color = d3.color(baseColor);
94 + // Calcular luminosidad relativa (fórmula WCAG)
95 + const r = color.r / 255;
96 + const g = color.g / 255;
97 + const b = color.b / 255;
98 + const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
99 + // Si el fondo es claro (amarillo, verde claro), usar texto oscuro
100 + return luminance > 0.5 ? 'rgba(0,0,0,0.85)' : 'rgba(255,255,255,0.95)';
101 + }
102 +
103 + function getTextColorSecondary(objeto) {
104 + const grupoDigit = String(objeto).charAt(0);
105 + const baseColor = GRUPO_COLORS[grupoDigit] || '#888888';
106 + const color = d3.color(baseColor);
107 + const r = color.r / 255;
108 + const g = color.g / 255;
109 + const b = color.b / 255;
110 + const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
111 + return luminance > 0.5 ? 'rgba(0,0,0,0.65)' : 'rgba(255,255,255,0.75)';
112 + }
113 +
114 + // Formatear números en bolivianos
115 + function formatMoney(value) {
116 + if (value >= 1e9) return `Bs ${(value / 1e9).toFixed(1)}MM`;
117 + if (value >= 1e6) return `Bs ${(value / 1e6).toFixed(1)}M`;
118 + if (value >= 1e3) return `Bs ${(value / 1e3).toFixed(0)}K`;
119 + return `Bs ${value.toFixed(0)}`;
120 + }
121 +
122 + // Formatear per cápita (población ~10M)
123 + const POBLACION = 10000000;
124 + function formatPerCapita(value) {
125 + const perCapita = value / POBLACION;
126 + if (perCapita >= 1000) return `Bs ${(perCapita / 1000).toFixed(1)}K/hab`;
127 + if (perCapita >= 1) return `Bs ${perCapita.toFixed(0)}/hab`;
128 + return `Bs ${perCapita.toFixed(2)}/hab`;
129 + }
130 +
131 + // Cargar datos del treemap
132 + async function loadTreemapData() {
133 + // Limpiar estado antes de cargar nuevos datos
134 + treemapNodes = [];
135 +
136 + try {
137 + const response = await fetch('/treemap_objeto_estado.csv');
138 + const csvText = await response.text();
139 + const parsed = d3.csvParse(csvText, d => ({
140 + gestion: +d.gestion,
141 + nivel: d.nivel,
142 + objeto: d.objeto,
143 + desc_objeto: d.desc_objeto === 'NA' ? null : d.desc_objeto,
144 + parent: d.parent === 'NA' ? null : d.parent,
145 + devengado: +d.devengado || 0
146 + }));
147 +
148 + // Filtrar por año y eliminar subpartidas duplicadas (las que tienen desc_objeto = null)
149 + // Estas son partidas sin desglose que aparecen duplicadas
150 + const filtered = parsed.filter(d =>
151 + d.gestion === selectedYear &&
152 + d.devengado > 0 &&
153 + !(d.nivel === 'subpartida' && d.desc_objeto === null)
154 + );
155 +
156 + treemapData = filtered;
157 + console.log('Treemap data loaded:', treemapData.length, 'items');
158 + buildTreemapHierarchy();
159 + } catch (err) {
160 + console.error('Error loading treemap data:', err);
161 + }
162 + }
163 +
164 + // Construir jerarquía D3
165 + function buildTreemapHierarchy() {
166 + if (!treemapData.length) {
167 + console.log('No treemap data to build hierarchy');
168 + return;
169 + }
170 +
171 + // Si es vista aplanada por nivel, usar lógica diferente
172 + if (treemapViewLevel !== 'jerarquico') {
173 + buildFlatTreemap();
174 + return;
175 + }
176 +
177 + // === MODO JERÁRQUICO (drill-down) ===
178 + const stratify = d3.stratify()
179 + .id(d => d.objeto)
180 + .parentId(d => d.parent);
181 +
182 + // Agregar nodo raíz virtual
183 + const dataWithRoot = [
184 + { objeto: 'root', parent: null, desc_objeto: 'Total Presupuesto', devengado: 0, nivel: 'root' },
185 + ...treemapData.map(d => ({
186 + ...d,
187 + parent: d.parent || 'root'
188 + }))
189 + ];
190 +
191 + // Verificar IDs duplicados
192 + const ids = dataWithRoot.map(d => d.objeto);
193 + const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
194 + if (duplicates.length > 0) {
195 + console.error('Duplicate IDs found:', [...new Set(duplicates)]);
196 + }
197 +
198 + try {
199 + const root = stratify(dataWithRoot);
200 +
201 + // Sumar valores: usar devengado de nodos hoja (sin hijos)
202 + root.sum(d => d.devengado)
203 + .sort((a, b) => b.value - a.value);
204 +
205 + // Eliminar doble conteo: solo contar hojas reales
206 + root.eachAfter(node => {
207 + if (node.children && node.children.length > 0) {
208 + node.value = node.children.reduce((sum, child) => sum + child.value, 0);
209 + }
210 + });
211 +
212 + treemapRoot = root;
213 + currentTreemapNode = root;
214 + treemapBreadcrumb = [{ id: 'root', name: 'Total' }];
215 +
216 + calculateTreemapLayout();
217 + } catch (err) {
218 + console.error('Error building hierarchy:', err);
219 + }
220 + }
221 +
222 + // Construir treemap aplanado por nivel
223 + function buildFlatTreemap() {
224 + // Filtrar solo items del nivel seleccionado
225 + const levelItems = treemapData.filter(d => d.nivel === treemapViewLevel);
226 +
227 + if (!levelItems.length) {
228 + console.log('No items for level:', treemapViewLevel);
229 + treemapNodes = [];
230 + return;
231 + }
232 +
233 + // Agrupar por grupo (primer dígito) para crear estructura anidada
234 + const gruposMap = new Map();
235 +
236 + // Primero obtener info de los grupos
237 + const gruposData = treemapData.filter(d => d.nivel === 'grupo');
238 +
239 + levelItems.forEach(item => {
240 + const grupoCode = String(item.objeto).charAt(0);
241 + if (!gruposMap.has(grupoCode)) {
242 + const grupoInfo = gruposData.find(g => g.objeto === grupoCode + '0000') || { desc_objeto: `Grupo ${grupoCode}` };
243 + gruposMap.set(grupoCode, {
244 + id: `grupo_${grupoCode}`,
245 + name: grupoInfo.desc_objeto,
246 + grupoCode,
247 + children: []
248 + });
249 + }
250 + gruposMap.get(grupoCode).children.push({
251 + id: item.objeto,
252 + name: item.desc_objeto || item.objeto,
253 + value: item.devengado,
254 + data: item,
255 + grupoCode
256 + });
257 + });
258 +
259 + // Crear jerarquía con grupos como contenedores
260 + const hierarchyData = {
261 + id: 'root',
262 + name: 'Total',
263 + children: Array.from(gruposMap.values()).map(grupo => ({
264 + ...grupo,
265 + children: grupo.children.sort((a, b) => b.value - a.value)
266 + }))
267 + };
268 +
269 + // Ordenar grupos por valor total
270 + hierarchyData.children.forEach(grupo => {
271 + grupo.totalValue = grupo.children.reduce((sum, child) => sum + child.value, 0);
272 + });
273 + hierarchyData.children.sort((a, b) => b.totalValue - a.totalValue);
274 +
275 + const root = d3.hierarchy(hierarchyData);
276 + // Solo sumar valores de hojas (items), no de grupos
277 + root.sum(d => d.children ? 0 : (d.value || 0));
278 +
279 + treemapRoot = root;
280 + currentTreemapNode = root;
281 + treemapBreadcrumb = [{ id: 'root', name: `Todos los ${NIVEL_OPTIONS.find(o => o.value === treemapViewLevel)?.label || 'items'}` }];
282 +
283 + calculateFlatTreemapLayout();
284 + }
285 +
286 + // Layout especial para vista aplanada (muestra grupos como secciones)
287 + function calculateFlatTreemapLayout() {
288 + if (!currentTreemapNode || !currentTreemapNode.children) {
289 + treemapNodes = [];
290 + return;
291 + }
292 +
293 + const treemap = d3.treemap()
294 + .size([treemapWidth, treemapHeight])
295 + .paddingOuter(4)
296 + .paddingTop(22) // Espacio para el título del grupo
297 + .paddingInner(2)
298 + .round(true);
299 +
300 + treemap(currentTreemapNode);
301 +
302 + // Extraer nodos: grupos (para títulos) e items individuales
303 + const nodes = [];
304 +
305 + currentTreemapNode.children.forEach(grupoNode => {
306 + // Agregar el grupo como "contenedor" con título
307 + nodes.push({
308 + type: 'grupo',
309 + id: grupoNode.data.id,
310 + name: grupoNode.data.name,
311 + grupoCode: grupoNode.data.grupoCode,
312 + x0: grupoNode.x0,
313 + y0: grupoNode.y0,
314 + x1: grupoNode.x1,
315 + y1: grupoNode.y1,
316 + value: grupoNode.value
317 + });
318 +
319 + // Agregar los items del grupo
320 + if (grupoNode.children) {
321 + grupoNode.children.forEach(itemNode => {
322 + nodes.push({
323 + type: 'item',
324 + id: itemNode.data.id,
325 + name: itemNode.data.name,
326 + grupoCode: grupoNode.data.grupoCode,
327 + x0: itemNode.x0,
328 + y0: itemNode.y0,
329 + x1: itemNode.x1,
330 + y1: itemNode.y1,
331 + value: itemNode.value,
332 + data: itemNode.data.data || itemNode.data
333 + });
334 + });
335 + }
336 + });
337 +
338 + treemapNodes = nodes;
339 + console.log('Flat layout calculated:', nodes.length, 'nodes');
340 + }
341 +
342 + // Calcular layout del treemap (modo jerárquico)
343 + function calculateTreemapLayout() {
344 + if (!currentTreemapNode || !currentTreemapNode.children || currentTreemapNode.children.length === 0) {
345 + treemapNodes = [];
346 + return;
347 + }
348 +
349 + if (treemapWidth <= 0 || treemapHeight <= 0) {
350 + return;
351 + }
352 +
353 + try {
354 + const treemap = d3.treemap()
355 + .size([treemapWidth, treemapHeight])
356 + .padding(3)
357 + .paddingTop(20)
358 + .round(true);
359 +
360 + // Crear una nueva jerarquía temporal para el layout
361 + // Esto asegura que el nodo tenga coordenadas válidas
362 + const tempHierarchy = d3.hierarchy({
363 + id: currentTreemapNode.id,
364 + data: currentTreemapNode.data,
365 + children: currentTreemapNode.children.map(child => ({
366 + id: child.id,
367 + data: child.data,
368 + value: child.value,
369 + hasChildren: child.children && child.children.length > 0,
370 + _ref: child // Referencia al nodo original
371 + }))
372 + });
373 +
374 + // Sumar valores (solo hojas)
375 + tempHierarchy.sum(d => d.children ? 0 : (d.value || 0));
376 + tempHierarchy.sort((a, b) => b.value - a.value);
377 +
378 + // Aplicar layout
379 + treemap(tempHierarchy);
380 +
381 + // Extraer los nodos hijos con coordenadas calculadas
382 + treemapNodes = tempHierarchy.children.map(child => ({
383 + x0: child.x0,
384 + y0: child.y0,
385 + x1: child.x1,
386 + y1: child.y1,
387 + id: child.data.id,
388 + data: child.data.data,
389 + value: child.value,
390 + _originalNode: child.data._ref // Usar la referencia al nodo original
391 + }));
392 + } catch (err) {
393 + console.error('Error calculating layout:', err);
394 + treemapNodes = [];
395 + }
396 + }
397 +
398 + // Navegar hacia abajo (drill-down)
399 + function drillDown(node) {
400 + const originalNode = node._originalNode || node;
401 + if (!originalNode.children || originalNode.children.length === 0) return;
402 +
403 + currentTreemapNode = originalNode;
404 + treemapBreadcrumb = [...treemapBreadcrumb, {
405 + id: originalNode.id,
406 + name: originalNode.data.desc_objeto || originalNode.id
407 + }];
408 + calculateTreemapLayout();
409 + }
410 +
411 + // Navegar hacia arriba
412 + function drillUp(index) {
413 + if (index === 0) {
414 + currentTreemapNode = treemapRoot;
415 + treemapBreadcrumb = [{ id: 'root', name: 'Total' }];
416 + } else {
417 + // Encontrar el nodo en el breadcrumb
418 + const targetId = treemapBreadcrumb[index].id;
419 + let node = treemapRoot;
420 + for (let i = 1; i <= index; i++) {
421 + const nextId = treemapBreadcrumb[i].id;
422 + node = node.children?.find(c => c.id === nextId) || node;
423 + }
424 + currentTreemapNode = node;
425 + treemapBreadcrumb = treemapBreadcrumb.slice(0, index + 1);
426 + }
427 + calculateTreemapLayout();
428 + }
429 +
430 + // Reactivo: recargar datos cuando cambia el año o nivel
431 + // Usar variable derivada para evitar loops en effects
432 + let treemapLoadTrigger = $derived(`${selectedYear}-${treemapViewLevel}-${viewMode}`);
433 + let lastTrigger = '';
434 +
435 + $effect(() => {
436 + const trigger = treemapLoadTrigger;
437 + if (viewMode === 'mapa' && selectedYear && trigger !== lastTrigger) {
438 + lastTrigger = trigger;
439 + loadTreemapData();
440 + }
441 + });
442 +
443 + // Función para recalcular layout según el modo actual
444 + function recalculateCurrentLayout() {
445 + try {
446 + if (treemapViewLevel === 'jerarquico') {
447 + if (currentTreemapNode && currentTreemapNode.children) {
448 + calculateTreemapLayout();
449 + }
450 + } else {
451 + if (currentTreemapNode && currentTreemapNode.children) {
452 + calculateFlatTreemapLayout();
453 + }
454 + }
455 + } catch (err) {
456 + console.error('Error recalculating layout:', err);
457 + }
458 + }
459 +
460 + // Manejar resize del contenedor (solo una vez al montar)
461 + let resizeObserver = null;
462 + let scrollHandler = null;
463 +
464 + $effect(() => {
465 + if (treemapContainer && viewMode === 'mapa') {
466 + // Función para actualizar dimensiones y recalcular layout
467 + const updateDimensions = () => {
468 + const rect = treemapContainer.getBoundingClientRect();
469 + if (rect.width > 0) {
470 + const newWidth = Math.floor(rect.width);
471 + // Calcular altura disponible para que quepa en viewport
472 + const viewportHeight = window.innerHeight;
473 + const paddingBottom = 24;
474 +
475 + const effectiveTop = Math.max(rect.top, 0);
476 + const availableHeight = viewportHeight - effectiveTop - paddingBottom;
477 + const newHeight = Math.max(250, Math.floor(availableHeight));
478 +
479 + if (newWidth !== treemapWidth || newHeight !== treemapHeight) {
480 + treemapWidth = newWidth;
481 + treemapHeight = newHeight;
482 + recalculateCurrentLayout();
483 + }
484 + }
485 + };
486 +
487 + // Calcular dimensiones iniciales con pequeño delay para asegurar render
488 + setTimeout(updateDimensions, 50);
489 +
490 + // Usar ResizeObserver para cambios de tamaño
491 + resizeObserver = new ResizeObserver(() => {
492 + updateDimensions();
493 + });
494 + resizeObserver.observe(treemapContainer);
495 +
496 + // También escuchar scroll para recalcular cuando el usuario hace scroll
497 + scrollHandler = () => {
498 + updateDimensions();
499 + };
500 + window.addEventListener('scroll', scrollHandler, { passive: true });
501 +
502 + return () => {
503 + if (resizeObserver) {
504 + resizeObserver.disconnect();
505 + }
506 + if (scrollHandler) {
507 + window.removeEventListener('scroll', scrollHandler);
508 + }
509 + };
510 + }
511 + });
12 512
13 onMount(async () => { 513 onMount(async () => {
514 + // Cargar clasificador de objetos
14 const { data, error } = await supabase 515 const { data, error } = await supabase
15 .schema('ppto') 516 .schema('ppto')
16 .from('clas_objetos') 517 .from('clas_objetos')
...@@ -37,9 +538,148 @@ ...@@ -37,9 +538,148 @@
37 } 538 }
38 } 539 }
39 540
541 + // Cargar años disponibles desde el CSV del treemap
542 + try {
543 + const response = await fetch('/treemap_objeto_estado.csv');
544 + const csvText = await response.text();
545 + const parsed = d3.csvParse(csvText);
546 + const uniqueYears = [...new Set(parsed.map(d => +d.gestion))].sort((a, b) => b - a);
547 + availableYears = uniqueYears;
548 + selectedYear = availableYears[0]; // Año más reciente por defecto
549 + } catch (err) {
550 + // Fallback si falla la carga
551 + availableYears = [2025];
552 + selectedYear = 2025;
553 + }
554 +
555 + // Cargar entidades para el selector
556 + const { data: entidadesData } = await supabase
557 + .schema('ppto')
558 + .from('clas_institucional')
559 + .select('entidad, desc_entidad, sigla_entidad, desc_area')
560 + .order('desc_entidad');
561 +
562 + if (entidadesData) {
563 + entities = entidadesData;
564 + }
565 +
40 loading = false; 566 loading = false;
41 }); 567 });
42 568
569 + // Filtrar entidades por búsqueda
570 + let filteredEntities = $derived(() => {
571 + if (!entitySearchQuery || entitySearchQuery.length < 2) {
572 + return entities.slice(0, 20); // Mostrar primeras 20 si no hay búsqueda
573 + }
574 + const q = entitySearchQuery.toLowerCase();
575 + return entities
576 + .filter(e =>
577 + e.desc_entidad?.toLowerCase().includes(q) ||
578 + e.sigla_entidad?.toLowerCase().includes(q) ||
579 + e.entidad?.toString().includes(q)
580 + )
581 + .slice(0, 20);
582 + });
583 +
584 + function selectEntity(entity) {
585 + selectedEntity = entity;
586 + entityDropdownOpen = false;
587 + entitySearchQuery = '';
588 + }
589 +
590 + function clearEntity() {
591 + selectedEntity = null;
592 + entityDropdownOpen = false;
593 + entitySearchQuery = '';
594 + }
595 +
596 + // Filtrar entidades para comparación A
597 + let filteredEntitiesA = $derived(() => {
598 + if (!compareASearchQuery || compareASearchQuery.length < 2) {
599 + return entities.slice(0, 20);
600 + }
601 + const q = compareASearchQuery.toLowerCase();
602 + return entities
603 + .filter(e =>
604 + e.desc_entidad?.toLowerCase().includes(q) ||
605 + e.sigla_entidad?.toLowerCase().includes(q) ||
606 + e.entidad?.toString().includes(q)
607 + )
608 + .slice(0, 20);
609 + });
610 +
611 + // Filtrar entidades para comparación B
612 + let filteredEntitiesB = $derived(() => {
613 + if (!compareBSearchQuery || compareBSearchQuery.length < 2) {
614 + return entities.slice(0, 20);
615 + }
616 + const q = compareBSearchQuery.toLowerCase();
617 + return entities
618 + .filter(e =>
619 + e.desc_entidad?.toLowerCase().includes(q) ||
620 + e.sigla_entidad?.toLowerCase().includes(q) ||
621 + e.entidad?.toString().includes(q)
622 + )
623 + .slice(0, 20);
624 + });
625 +
626 + // Inicializar modo comparar con valores inteligentes
627 + function initCompareMode() {
628 + // Lado A: usar selección actual de Mapa, o defaults
629 + compareA = {
630 + year: selectedYear || availableYears[0],
631 + entity: selectedEntity
632 + };
633 + // Lado B: misma entidad, año anterior
634 + const yearIndex = availableYears.indexOf(compareA.year);
635 + compareB = {
636 + year: availableYears[yearIndex + 1] || availableYears[0],
637 + entity: selectedEntity
638 + };
639 + }
640 +
641 + function selectCompareEntity(side, entity) {
642 + if (side === 'A') {
643 + compareA = { ...compareA, entity };
644 + compareADropdownOpen = false;
645 + compareASearchQuery = '';
646 + } else {
647 + compareB = { ...compareB, entity };
648 + compareBDropdownOpen = false;
649 + compareBSearchQuery = '';
650 + }
651 + }
652 +
653 + function clearCompareEntity(side) {
654 + if (side === 'A') {
655 + compareA = { ...compareA, entity: null };
656 + compareADropdownOpen = false;
657 + compareASearchQuery = '';
658 + } else {
659 + compareB = { ...compareB, entity: null };
660 + compareBDropdownOpen = false;
661 + compareBSearchQuery = '';
662 + }
663 + }
664 +
665 + function swapCompareSides() {
666 + const temp = compareA;
667 + compareA = compareB;
668 + compareB = temp;
669 + }
670 +
671 + function handleClickOutsideEntity(e) {
672 + if (entityDropdownOpen && !e.target.closest('.entity-dropdown-container')) {
673 + entityDropdownOpen = false;
674 + }
675 + if (compareADropdownOpen && !e.target.closest('.compare-a-dropdown')) {
676 + compareADropdownOpen = false;
677 + }
678 + if (compareBDropdownOpen && !e.target.closest('.compare-b-dropdown')) {
679 + compareBDropdownOpen = false;
680 + }
681 + }
682 +
43 function getItemsForGrupo(grupoCode) { 683 function getItemsForGrupo(grupoCode) {
44 if (!grupoCode) return { subgrupos: [] }; 684 if (!grupoCode) return { subgrupos: [] };
45 685
...@@ -134,6 +774,7 @@ ...@@ -134,6 +774,7 @@
134 selectedItem = null; 774 selectedItem = null;
135 searchQuery = ''; 775 searchQuery = '';
136 highlightedItem = null; 776 highlightedItem = null;
777 + sidebarOpen = false; // Cerrar sidebar en móvil al seleccionar
137 } 778 }
138 779
139 function openDetail(item) { 780 function openDetail(item) {
...@@ -173,85 +814,389 @@ ...@@ -173,85 +814,389 @@
173 let grupoContent = $derived(selectedGrupo ? getItemsForGrupo(selectedGrupo.objeto) : { subgrupos: [] }); 814 let grupoContent = $derived(selectedGrupo ? getItemsForGrupo(selectedGrupo.objeto) : { subgrupos: [] });
174 let searchResults = $derived(searchGlobal(searchQuery)); 815 let searchResults = $derived(searchGlobal(searchQuery));
175 let isSearching = $derived(searchQuery.length >= 2); 816 let isSearching = $derived(searchQuery.length >= 2);
817 +
818 + // Contadores
819 + let totalItems = $derived(allItems.length);
820 + let countByNivel = $derived({
821 + grupos: allItems.filter(i => i.nivel === 'grupo').length,
822 + subgrupos: allItems.filter(i => i.nivel === 'subgrupo').length,
823 + partidas: allItems.filter(i => i.nivel === 'partida').length,
824 + subpartidas: allItems.filter(i => i.nivel === 'subpartida').length
825 + });
176 </script> 826 </script>
177 827
178 <svelte:head> 828 <svelte:head>
179 <title>Objeto del Gasto | Presupuesto Público</title> 829 <title>Objeto del Gasto | Presupuesto Público</title>
830 + <link rel="preconnect" href="https://fonts.googleapis.com" />
831 + <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" />
180 </svelte:head> 832 </svelte:head>
181 833
182 -<div class="min-h-screen bg-white"> 834 +<svelte:window onclick={handleClickOutsideEntity} />
183 - <!-- Header --> 835 +
184 - <header class="border-b bg-slate-50"> 836 +<div class="min-h-screen" style="font-family: 'Instrument Sans', sans-serif; background-color: var(--theme-body); color: var(--theme-titulo); transition: background-color 0.2s, color 0.2s;">
185 - <div class="max-w-screen-2xl mx-auto px-6 py-4"> 837 + <!-- Header pedagógico -->
186 - <nav class="text-sm text-slate-500 mb-2"> 838 + <header class="border-b" style="border-color: var(--theme-borde); background-color: var(--theme-body);">
187 - <a href="/" class="hover:text-slate-700">Inicio</a> 839 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-6">
188 - <span class="mx-2">/</span> 840 + <!-- Breadcrumb: responsive -->
189 - <a href="/clasificadores" class="hover:text-slate-700">Clasificadores</a> 841 + <nav class="mb-4" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;">
190 - <span class="mx-2">/</span> 842 + <!-- Móvil: solo padre -->
191 - <span class="text-slate-900">Objeto del Gasto</span> 843 + <a href="/clasificadores" class="sm:hidden transition-colors" style="color: var(--theme-texto);">
844 + ← Clasificadores
845 + </a>
846 + <!-- Desktop: ruta completa -->
847 + <div class="hidden sm:flex items-center gap-2" style="color: var(--theme-texto);">
848 + <a href="/" class="transition-colors hover:opacity-80">Inicio</a>
849 + <span style="opacity: 0.5;">/</span>
850 + <a href="/clasificadores" class="transition-colors hover:opacity-80">Clasificadores</a>
851 + </div>
192 </nav> 852 </nav>
193 - <div class="flex items-center justify-between"> 853 +
194 - <h1 class="text-2xl font-light text-slate-900">Clasificador por Objeto del Gasto</h1> 854 + <div class="max-w-3xl">
855 + <p class="text-xs uppercase tracking-widest mb-2" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
856 + Clasificador 03
857 + </p>
858 + <h1 class="text-3xl mb-3" style="font-family: 'DM Serif Display', serif; color: var(--theme-titulo);">
859 + ¿En qué se gasta?
860 + </h1>
861 + <p class="leading-relaxed mb-3" style="color: var(--theme-texto);">
862 + Ordena el gasto según <strong style="color: var(--theme-titulo);">qué se compra o paga</strong>:
863 + sueldos, alquileres, combustible, medicamentos, construcciones, deudas, transferencias.
864 + Es la forma más directa de entender en qué se usa el dinero público.
865 + </p>
866 +
867 + {#if totalItems > 0}
868 + <p class="text-sm mb-5" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
869 + <span style="color: var(--theme-titulo); font-weight: 500;">{totalItems}</span> categorías de gasto:
870 + {countByNivel.grupos} grupos, {countByNivel.subgrupos} subgrupos, {countByNivel.partidas} partidas, {countByNivel.subpartidas} subpartidas
871 + </p>
872 + {/if}
873 +
874 + <!-- Jerarquía -->
875 + <div class="hidden sm:flex flex-wrap items-center gap-2 sm:gap-3 text-xs mb-6" style="font-family: 'DM Mono', monospace;">
876 + <span class="px-2.5 py-1 rounded-full font-medium" style="background-color: var(--theme-accent); color: var(--theme-body); opacity: 0.9;">
877 + Grupo
878 + </span>
879 + <span style="color: var(--theme-texto);">→</span>
880 + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">
881 + Subgrupo
882 + </span>
883 + <span style="color: var(--theme-texto);">→</span>
884 + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">
885 + Partida
886 + </span>
887 + <span style="color: var(--theme-texto);">→</span>
888 + <span class="px-2.5 py-1 rounded-full" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">
889 + Subpartida
890 + </span>
891 + </div>
892 + <!-- Versión móvil simplificada -->
893 + <p class="sm:hidden text-sm mb-4" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">
894 + Jerarquía: Grupo → Subgrupo → Partida → Subpartida
895 + </p>
896 + </div>
897 +
898 + <!-- Controles de visualización (fuera del max-w-3xl para usar todo el ancho) -->
899 + <div class="mt-8">
900 + <!-- Fila principal: Toggle + Guía + Filtros -->
901 + <div class="flex flex-col md:flex-row gap-4 md:items-center md:justify-between">
902 + <!-- Toggle + Guía inline -->
903 + <div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
904 + <!-- Toggle Vista -->
905 + <div class="flex items-center gap-3">
906 + <span class="text-sm font-medium" style="color: var(--theme-titulo);">Explorar como</span>
907 + <nav class="inline-flex rounded-xl border-2 p-1 shadow-sm" style="border-color: var(--theme-titulo); background-color: var(--theme-surface);">
908 + <a
909 + href="?modo=lista"
910 + class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
911 + style="{viewMode === 'lista' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
912 + onclick={(e) => { e.preventDefault(); setMode('lista'); }}
913 + >
914 + Lista
915 + </a>
916 + <a
917 + href="?modo=mapa"
918 + class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
919 + style="{viewMode === 'mapa' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
920 + onclick={(e) => { e.preventDefault(); setMode('mapa'); }}
921 + >
922 + Mapa
923 + </a>
924 + <a
925 + href="?modo=comparar"
926 + class="px-4 py-2 text-sm font-semibold rounded-lg transition-all"
927 + style="{viewMode === 'comparar' ? `background-color: var(--theme-titulo); color: var(--theme-body);` : `color: var(--theme-texto);`}"
928 + onclick={(e) => { e.preventDefault(); setMode('comparar'); initCompareMode(); }}
929 + >
930 + Comparar
931 + </a>
932 + </nav>
933 + </div>
934 +
935 + <!-- Guía inline (visible en sm+) -->
936 + <div class="hidden sm:block text-sm border-l pl-4" style="color: var(--theme-texto); border-color: var(--theme-borde);">
937 + {#if viewMode === 'lista'}
938 + <span class="flex items-center gap-1.5">
939 + Clic en
940 + <span class="inline-flex items-center justify-center w-5 h-5 rounded" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">
941 + <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
942 + <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" />
943 + </svg>
944 + </span>
945 + para ver el presupuesto histórico ·
946 + <span class="underline decoration-dotted font-medium" style="color: var(--theme-accent);">N var.</span>
947 + = número de cambios en la descripción oficial
948 + </span>
949 + {:else if viewMode === 'mapa'}
950 + <span class="flex items-center gap-1.5">
951 + {#if treemapViewLevel === 'jerarquico'}
952 + Tamaño = peso en el presupuesto · Clic en
953 + <span class="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">+</span>
954 + para ver desglose
955 + {:else}
956 + Vista aplanada · Colores agrupan por categoría principal
957 + {/if}
958 + </span>
959 + {:else}
960 + <span>Selecciona año y entidad para cada lado</span>
961 + {/if}
962 + </div>
963 + </div>
964 +
965 + <!-- Filtros (solo en modo mapa) -->
966 + {#if viewMode === 'mapa'}
967 + <div class="flex flex-wrap items-center gap-3 flex-shrink-0">
968 + <!-- Selector de nivel -->
969 + <div class="flex items-center gap-2">
970 + <label for="level-select" class="text-xs uppercase tracking-wide" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">Vista</label>
971 + <select
972 + id="level-select"
973 + bind:value={treemapViewLevel}
974 + class="px-3 py-1.5 text-sm rounded-lg focus:outline-none focus:ring-2 cursor-pointer"
975 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
976 + >
977 + {#each NIVEL_OPTIONS as opt}
978 + <option value={opt.value}>{opt.label}</option>
979 + {/each}
980 + </select>
981 + </div>
982 +
983 + <div class="flex items-center gap-2">
984 + <label for="year-select" class="text-xs uppercase tracking-wide" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">Año</label>
985 + <select
986 + id="year-select"
987 + bind:value={selectedYear}
988 + class="px-3 py-1.5 text-sm rounded-lg focus:outline-none focus:ring-2 cursor-pointer"
989 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
990 + >
991 + {#each availableYears as year}
992 + <option value={year}>{year}</option>
993 + {/each}
994 + </select>
995 + </div>
996 +
997 + <div class="flex items-center gap-2 entity-dropdown-container">
998 + <label class="text-xs uppercase tracking-wide" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);">Entidad</label>
999 + <div class="relative">
1000 + <button
1001 + class="px-3 py-1.5 text-sm rounded-lg focus:outline-none focus:ring-2 min-w-[180px] text-left flex items-center justify-between gap-2"
1002 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
1003 + onclick={() => entityDropdownOpen = !entityDropdownOpen}
1004 + >
1005 + <span class="truncate">
1006 + {#if selectedEntity}
1007 + {selectedEntity.sigla_entidad || selectedEntity.desc_entidad?.slice(0, 25)}
1008 + {:else}
1009 + Todo el Estado
1010 + {/if}
1011 + </span>
1012 + <svg class="w-4 h-4 shrink-0" style="color: var(--theme-texto);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1013 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1014 + </svg>
1015 + </button>
1016 +
1017 + {#if entityDropdownOpen}
1018 + <div class="absolute top-full left-0 mt-1 w-80 rounded-lg shadow-lg z-50 max-h-72 overflow-hidden" style="background-color: var(--theme-surface); border: 1px solid var(--theme-borde);">
1019 + <div class="p-2 border-b" style="border-color: var(--theme-borde);">
1020 + <input
1021 + type="text"
1022 + bind:value={entitySearchQuery}
1023 + placeholder="Buscar entidad..."
1024 + class="w-full px-3 py-1.5 text-sm rounded focus:outline-none focus:ring-2"
1025 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-fill); color: var(--theme-titulo);"
1026 + />
1027 + </div>
1028 + <button
1029 + class="w-full text-left px-3 py-2 text-sm border-b"
1030 + style="{!selectedEntity ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent); color: var(--theme-titulo);` : `color: var(--theme-titulo);`} border-color: var(--theme-borde);"
1031 + onclick={() => clearEntity()}
1032 + >
1033 + <span class="font-medium">Todo el Estado</span>
1034 + <span class="text-xs ml-2" style="color: var(--theme-texto);">(agregado nacional)</span>
1035 + </button>
1036 + <div class="overflow-y-auto max-h-48">
1037 + {#each filteredEntities() as entity}
1038 + <button
1039 + class="w-full text-left px-3 py-2 text-sm"
1040 + style="{selectedEntity?.entidad === entity.entidad ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);` : ``} color: var(--theme-titulo);"
1041 + onclick={() => selectEntity(entity)}
1042 + >
1043 + <div class="flex items-center gap-2">
1044 + <span class="font-mono text-xs" style="color: var(--theme-texto);">{entity.entidad}</span>
1045 + <span class="truncate">{entity.desc_entidad}</span>
1046 + {#if entity.sigla_entidad}
1047 + <span class="text-xs" style="color: var(--theme-texto);">({entity.sigla_entidad})</span>
1048 + {/if}
1049 + </div>
1050 + </button>
1051 + {/each}
1052 + </div>
1053 + </div>
1054 + {/if}
1055 + </div>
1056 + </div>
1057 + </div>
1058 + {/if}
1059 + </div>
1060 +
1061 + <!-- Guía móvil (solo visible en móvil, no en comparar) -->
1062 + {#if viewMode !== 'comparar'}
1063 + <div class="sm:hidden mt-3 pt-3 border-t text-sm" style="border-color: var(--theme-borde); color: var(--theme-texto);">
1064 + {#if viewMode === 'lista'}
1065 + <p class="flex flex-wrap items-center gap-1.5">
1066 + Clic en
1067 + <span class="inline-flex items-center justify-center w-5 h-5 rounded" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">
1068 + <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1069 + <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" />
1070 + </svg>
1071 + </span>
1072 + para ver el presupuesto histórico.
1073 + <span class="underline decoration-dotted font-medium" style="color: var(--theme-accent);">N var.</span>
1074 + = número de cambios en la descripción oficial.
1075 + </p>
1076 + {:else if viewMode === 'mapa'}
1077 + <p class="flex flex-wrap items-center gap-1.5">
1078 + {#if treemapViewLevel === 'jerarquico'}
1079 + Tamaño = peso · Clic en
1080 + <span class="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs font-bold" style="background-color: var(--theme-fill); color: var(--theme-titulo); border: 1px solid var(--theme-borde);">+</span>
1081 + para desglose.
1082 + {:else}
1083 + Colores agrupan por categoría.
1084 + {/if}
1085 + </p>
1086 + {:else}
1087 + <p>Selecciona año y entidad para cada lado.</p>
1088 + {/if}
1089 + </div>
1090 + {/if}
195 </div> 1091 </div>
196 </div> 1092 </div>
197 </header> 1093 </header>
198 1094
199 {#if loading} 1095 {#if loading}
200 <div class="flex items-center justify-center py-20"> 1096 <div class="flex items-center justify-center py-20">
201 - <p class="text-slate-500">Cargando clasificador...</p> 1097 + <p style="color: var(--theme-texto);">Cargando clasificador...</p>
202 </div> 1098 </div>
203 {:else} 1099 {:else}
204 - <div class="max-w-screen-2xl mx-auto flex"> 1100 + {#if viewMode === 'lista'}
1101 + <!-- ═══════════════════════════════════════════════════════════ -->
1102 + <!-- MODO LISTA: Navegación jerárquica de categorías de gasto -->
1103 + <!-- ═══════════════════════════════════════════════════════════ -->
1104 +
1105 + <!-- Botón móvil para abrir sidebar -->
1106 + <div class="lg:hidden px-4 py-3 border-b" style="border-color: var(--theme-borde); background-color: var(--theme-fill);">
1107 + <button
1108 + onclick={() => sidebarOpen = !sidebarOpen}
1109 + class="flex items-center gap-2 text-sm"
1110 + style="color: var(--theme-texto);"
1111 + >
1112 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1113 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
1114 + </svg>
1115 + <span class="font-medium">{selectedGrupo?.desc_objeto || 'Seleccionar grupo'}</span>
1116 + <svg class="w-4 h-4 ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1117 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1118 + </svg>
1119 + </button>
1120 + </div>
1121 +
1122 + <div class="max-w-screen-xl mx-auto flex px-4">
205 <!-- Sidebar izquierda: Grupos --> 1123 <!-- Sidebar izquierda: Grupos -->
206 - <aside class="w-64 flex-shrink-0 border-r bg-slate-50/50"> 1124 + <!-- En móvil: overlay, en desktop: sidebar fijo -->
207 - <div class="sticky top-0 h-screen overflow-y-auto p-4"> 1125 + {#if sidebarOpen}
1126 + <div class="fixed inset-0 bg-black/30 z-40 lg:hidden" onclick={() => sidebarOpen = false}></div>
1127 + {/if}
1128 + <aside class="
1129 + {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
1130 + lg:translate-x-0
1131 + fixed lg:relative
1132 + inset-y-0 left-0
1133 + w-72 lg:w-64
1134 + z-50 lg:z-auto
1135 + transition-transform duration-200 ease-in-out
1136 + lg:flex-shrink-0
1137 + shadow-xl lg:shadow-none
1138 + sidebar-left
1139 + ">
1140 + <div class="h-full lg:h-screen lg:sticky lg:top-0 overflow-y-auto py-6 px-4 lg:px-0 lg:pr-6">
1141 + <!-- Cerrar en móvil -->
1142 + <div class="flex justify-between items-center mb-4 lg:hidden">
1143 + <span class="text-sm font-medium" style="color: var(--theme-titulo);">Grupos de gasto</span>
1144 + <button onclick={() => sidebarOpen = false} style="color: var(--theme-texto);">
1145 + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1146 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
1147 + </svg>
1148 + </button>
1149 + </div>
1150 +
208 <!-- Buscador --> 1151 <!-- Buscador -->
209 <div class="mb-6"> 1152 <div class="mb-6">
210 <input 1153 <input
211 type="text" 1154 type="text"
212 bind:value={searchQuery} 1155 bind:value={searchQuery}
213 placeholder="Buscar..." 1156 placeholder="Buscar..."
214 - class="w-full px-3 py-2 text-sm border border-slate-200 rounded-md bg-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" 1157 + class="w-full px-3 py-2 text-sm rounded-md focus:outline-none focus:ring-2"
1158 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
215 /> 1159 />
216 </div> 1160 </div>
217 1161
218 <!-- Resultados de búsqueda --> 1162 <!-- Resultados de búsqueda -->
219 {#if isSearching} 1163 {#if isSearching}
220 <div class="mb-4"> 1164 <div class="mb-4">
221 - <p class="text-xs text-slate-500 uppercase tracking-wide mb-2"> 1165 + <p class="text-xs uppercase tracking-wide mb-2" style="color: var(--theme-texto);">
222 {searchResults.length} resultados 1166 {searchResults.length} resultados
223 </p> 1167 </p>
224 <div class="space-y-1"> 1168 <div class="space-y-1">
225 {#each searchResults as result} 1169 {#each searchResults as result}
226 <button 1170 <button
227 - class="w-full text-left px-2 py-2 text-sm rounded hover:bg-white hover:shadow-sm transition-all" 1171 + class="w-full text-left px-2 py-2 text-sm rounded transition-all"
1172 + style="color: var(--theme-titulo);"
228 onclick={() => goToSearchResult(result)} 1173 onclick={() => goToSearchResult(result)}
229 > 1174 >
230 - <span class="text-xs text-slate-400 block">{getNivelLabel(result.nivel)}</span> 1175 + <span class="text-xs block" style="color: var(--theme-texto);">{getNivelLabel(result.nivel)}</span>
231 - <span class="font-mono text-xs text-blue-600">{result.objeto}</span> 1176 + <span class="font-mono text-xs font-medium" style="color: var(--theme-accent);">{result.objeto}</span>
232 - <span class="text-slate-700 ml-1">{result.desc_objeto}</span> 1177 + <span class="ml-1">{result.desc_objeto}</span>
233 </button> 1178 </button>
234 {/each} 1179 {/each}
235 {#if searchResults.length === 0} 1180 {#if searchResults.length === 0}
236 - <p class="text-sm text-slate-400 px-2">Sin resultados</p> 1181 + <p class="text-sm px-2" style="color: var(--theme-texto);">Sin resultados</p>
237 {/if} 1182 {/if}
238 </div> 1183 </div>
239 </div> 1184 </div>
240 {:else} 1185 {:else}
241 <!-- Lista de grupos --> 1186 <!-- Lista de grupos -->
242 <nav> 1187 <nav>
243 - <p class="text-xs text-slate-500 uppercase tracking-wide mb-3 px-2">Grupos</p> 1188 + <p class="text-xs uppercase tracking-wide mb-3 px-2 hidden lg:block" style="color: var(--theme-texto);">Grupos</p>
244 <ul class="space-y-1"> 1189 <ul class="space-y-1">
245 {#each grupos as grupo} 1190 {#each grupos as grupo}
246 <li> 1191 <li>
247 <button 1192 <button
248 - class="w-full text-left px-3 py-2 rounded-md text-sm transition-all 1193 + class="w-full text-left px-3 py-2 rounded-md text-sm transition-all"
249 - {selectedGrupo?.objeto === grupo.objeto 1194 + style="{selectedGrupo?.objeto === grupo.objeto
250 - ? 'bg-blue-50 text-blue-900 font-medium border-l-2 border-blue-500' 1195 + ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent); color: var(--theme-titulo); font-weight: 500; border-left: 2px solid var(--theme-accent);`
251 - : 'text-slate-600 hover:bg-white hover:text-slate-900'}" 1196 + : `color: var(--theme-texto);`}"
252 onclick={() => selectGrupo(grupo)} 1197 onclick={() => selectGrupo(grupo)}
253 > 1198 >
254 - <span class="font-mono text-xs text-slate-400 block">{grupo.objeto}</span> 1199 + <span class="font-mono text-xs block" style="color: var(--theme-texto);">{grupo.objeto}</span>
255 {grupo.desc_objeto} 1200 {grupo.desc_objeto}
256 </button> 1201 </button>
257 </li> 1202 </li>
...@@ -263,17 +1208,18 @@ ...@@ -263,17 +1208,18 @@
263 </aside> 1208 </aside>
264 1209
265 <!-- Contenido principal --> 1210 <!-- Contenido principal -->
266 - <main class="flex-1 min-w-0"> 1211 + <main class="flex-1 min-w-0 lg:border-l" style="border-color: var(--theme-borde);">
267 - <div class="px-8 py-6"> 1212 + <div class="px-4 sm:px-6 lg:px-10 py-6 lg:py-8">
268 {#if selectedGrupo} 1213 {#if selectedGrupo}
269 <!-- Título del grupo --> 1214 <!-- Título del grupo -->
270 - <div class="mb-8 pb-6 border-b"> 1215 + <div class="mb-8 lg:mb-10 pb-6 lg:pb-8 border-b" style="border-color: var(--theme-borde);">
271 - <p class="text-sm font-mono text-slate-400 mb-1">{selectedGrupo.objeto}</p> 1216 + <p class="text-sm font-mono mb-1" style="color: var(--theme-texto);">{selectedGrupo.objeto}</p>
272 - <h2 class="text-xl font-medium text-slate-900 mb-2 flex items-center gap-3"> 1217 + <h2 class="text-lg sm:text-xl font-medium mb-2 flex items-center gap-2 sm:gap-3 flex-wrap" style="color: var(--theme-titulo);">
273 {selectedGrupo.desc_objeto} 1218 {selectedGrupo.desc_objeto}
274 <a 1219 <a
275 href="/objeto/{selectedGrupo.objeto}" 1220 href="/objeto/{selectedGrupo.objeto}"
276 - class="text-slate-300 hover:text-blue-500 transition-colors" 1221 + class="transition-colors hover:text-[var(--theme-accent)]"
1222 + style="color: var(--theme-texto);"
277 title="Ver detalle" 1223 title="Ver detalle"
278 > 1224 >
279 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1225 <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
...@@ -284,7 +1230,7 @@ ...@@ -284,7 +1230,7 @@
284 {#if selectedGrupo.descripciones} 1230 {#if selectedGrupo.descripciones}
285 {@const grupoDescs = parseDescripciones(selectedGrupo.descripciones)} 1231 {@const grupoDescs = parseDescripciones(selectedGrupo.descripciones)}
286 {#if grupoDescs.length > 0} 1232 {#if grupoDescs.length > 0}
287 - <p class="text-slate-600 leading-relaxed">{grupoDescs[0].descripcion}</p> 1233 + <p class="leading-relaxed" style="color: var(--theme-texto);">{grupoDescs[0].descripcion}</p>
288 {#if selectedGrupo.n_variaciones > 1} 1234 {#if selectedGrupo.n_variaciones > 1}
289 <button 1235 <button
290 class="text-sm text-orange-500 hover:text-orange-700 mt-2 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer" 1236 class="text-sm text-orange-500 hover:text-orange-700 mt-2 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer"
...@@ -298,17 +1244,18 @@ ...@@ -298,17 +1244,18 @@
298 </div> 1244 </div>
299 1245
300 <!-- Subgrupos --> 1246 <!-- Subgrupos -->
301 - <div class="space-y-8"> 1247 + <div class="space-y-12">
302 {#each grupoContent.subgrupos as subgrupo} 1248 {#each grupoContent.subgrupos as subgrupo}
303 {@const subgrupoDescs = parseDescripciones(subgrupo.descripciones)} 1249 {@const subgrupoDescs = parseDescripciones(subgrupo.descripciones)}
304 <section 1250 <section
305 id="sg-{subgrupo.objeto}" 1251 id="sg-{subgrupo.objeto}"
306 - class="scroll-mt-4 {highlightedItem === subgrupo.objeto ? 'ring-2 ring-blue-200 rounded-lg' : ''}" 1252 + class="scroll-mt-4"
1253 + style="{highlightedItem === subgrupo.objeto ? `box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); border-radius: 0.5rem;` : ''}"
307 > 1254 >
308 - <div class="flex items-start gap-4 mb-3"> 1255 + <div class="flex items-start gap-2 sm:gap-4 mb-3">
309 - <span class="font-mono text-sm text-slate-400 pt-1">{subgrupo.objeto}</span> 1256 + <span class="font-mono text-xs sm:text-sm pt-1" style="color: var(--theme-texto);">{subgrupo.objeto}</span>
310 <div class="flex-1"> 1257 <div class="flex-1">
311 - <h3 class="text-lg font-medium text-slate-900 flex items-center gap-2"> 1258 + <h3 class="text-base sm:text-lg font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);">
312 <span class="text-left">{subgrupo.desc_objeto}</span> 1259 <span class="text-left">{subgrupo.desc_objeto}</span>
313 {#if subgrupo.n_variaciones > 1} 1260 {#if subgrupo.n_variaciones > 1}
314 <button 1261 <button
...@@ -321,7 +1268,8 @@ ...@@ -321,7 +1268,8 @@
321 {/if} 1268 {/if}
322 <a 1269 <a
323 href="/objeto/{subgrupo.objeto}" 1270 href="/objeto/{subgrupo.objeto}"
324 - class="text-slate-300 hover:text-blue-500 transition-colors" 1271 + class="transition-colors hover:text-[var(--theme-accent)]"
1272 + style="color: var(--theme-texto);"
325 title="Ver página de detalle" 1273 title="Ver página de detalle"
326 > 1274 >
327 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1275 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
...@@ -330,24 +1278,25 @@ ...@@ -330,24 +1278,25 @@
330 </a> 1278 </a>
331 </h3> 1279 </h3>
332 {#if subgrupoDescs.length > 0} 1280 {#if subgrupoDescs.length > 0}
333 - <p class="text-sm text-slate-600 mt-1 leading-relaxed">{subgrupoDescs[0].descripcion}</p> 1281 + <p class="text-sm mt-1 leading-relaxed" style="color: var(--theme-texto);">{subgrupoDescs[0].descripcion}</p>
334 {/if} 1282 {/if}
335 </div> 1283 </div>
336 </div> 1284 </div>
337 1285
338 <!-- Partidas --> 1286 <!-- Partidas -->
339 {#if subgrupo.partidas?.length > 0} 1287 {#if subgrupo.partidas?.length > 0}
340 - <div class="ml-16 space-y-4 border-l-2 border-slate-100 pl-6"> 1288 + <div class="ml-4 sm:ml-8 lg:ml-16 space-y-5 border-l pl-4 sm:pl-6 lg:pl-8" style="border-color: var(--theme-borde);">
341 {#each subgrupo.partidas as partida} 1289 {#each subgrupo.partidas as partida}
342 {@const partidaDescs = parseDescripciones(partida.descripciones)} 1290 {@const partidaDescs = parseDescripciones(partida.descripciones)}
343 <div 1291 <div
344 id="p-{partida.objeto}" 1292 id="p-{partida.objeto}"
345 - class="scroll-mt-4 {highlightedItem === partida.objeto ? 'ring-2 ring-blue-200 rounded-lg p-2 -ml-2' : ''}" 1293 + class="scroll-mt-4"
1294 + style="{highlightedItem === partida.objeto ? `box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); border-radius: 0.5rem; padding: 0.5rem; margin-left: -0.5rem;` : ''}"
346 > 1295 >
347 - <div class="flex items-start gap-3"> 1296 + <div class="flex items-start gap-2 sm:gap-3">
348 - <span class="font-mono text-xs text-slate-400 pt-0.5">{partida.objeto}</span> 1297 + <span class="font-mono text-xs pt-0.5 shrink-0" style="color: var(--theme-texto);">{partida.objeto}</span>
349 - <div class="flex-1"> 1298 + <div class="flex-1 min-w-0">
350 - <h4 class="text-sm font-medium text-slate-800 flex items-center gap-2"> 1299 + <h4 class="text-sm font-medium flex items-center gap-2 flex-wrap" style="color: var(--theme-titulo);">
351 <span class="text-left">{partida.desc_objeto}</span> 1300 <span class="text-left">{partida.desc_objeto}</span>
352 {#if partida.n_variaciones > 1} 1301 {#if partida.n_variaciones > 1}
353 <button 1302 <button
...@@ -360,7 +1309,8 @@ ...@@ -360,7 +1309,8 @@
360 {/if} 1309 {/if}
361 <a 1310 <a
362 href="/objeto/{partida.objeto}" 1311 href="/objeto/{partida.objeto}"
363 - class="text-slate-300 hover:text-blue-500 transition-colors" 1312 + class="transition-colors hover:text-[var(--theme-accent)]"
1313 + style="color: var(--theme-texto);"
364 title="Ver página de detalle" 1314 title="Ver página de detalle"
365 > 1315 >
366 <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1316 <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
...@@ -369,25 +1319,26 @@ ...@@ -369,25 +1319,26 @@
369 </a> 1319 </a>
370 </h4> 1320 </h4>
371 {#if partidaDescs.length > 0} 1321 {#if partidaDescs.length > 0}
372 - <p class="text-xs text-slate-500 mt-1 leading-relaxed">{partidaDescs[0].descripcion}</p> 1322 + <p class="text-sm mt-1 leading-relaxed" style="color: var(--theme-texto);">{partidaDescs[0].descripcion}</p>
373 {/if} 1323 {/if}
374 </div> 1324 </div>
375 </div> 1325 </div>
376 1326
377 <!-- Subpartidas --> 1327 <!-- Subpartidas -->
378 {#if partida.subpartidas?.length > 0} 1328 {#if partida.subpartidas?.length > 0}
379 - <div class="ml-12 mt-3 space-y-2 border-l border-slate-100 pl-4"> 1329 + <div class="ml-4 sm:ml-8 lg:ml-14 mt-4 space-y-3 border-l pl-4 lg:pl-6" style="border-color: color-mix(in srgb, var(--theme-borde) 60%, transparent);">
380 {#each partida.subpartidas as subpartida} 1330 {#each partida.subpartidas as subpartida}
381 {@const subpartidaDescs = parseDescripciones(subpartida.descripciones)} 1331 {@const subpartidaDescs = parseDescripciones(subpartida.descripciones)}
382 <div 1332 <div
383 id="sp-{subpartida.objeto}" 1333 id="sp-{subpartida.objeto}"
384 - class="scroll-mt-4 {highlightedItem === subpartida.objeto ? 'ring-2 ring-blue-200 rounded p-1 -ml-1' : ''}" 1334 + class="scroll-mt-4"
1335 + style="{highlightedItem === subpartida.objeto ? `box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme-accent) 30%, transparent); border-radius: 0.25rem; padding: 0.25rem; margin-left: -0.25rem;` : ''}"
385 > 1336 >
386 <div class="flex items-start gap-2"> 1337 <div class="flex items-start gap-2">
387 - <span class="font-mono text-xs text-slate-300">{subpartida.objeto}</span> 1338 + <span class="font-mono text-xs" style="color: var(--theme-texto);">{subpartida.objeto}</span>
388 <div class="flex-1"> 1339 <div class="flex-1">
389 <span class="flex items-center gap-2"> 1340 <span class="flex items-center gap-2">
390 - <span class="text-xs text-slate-700">{subpartida.desc_objeto}</span> 1341 + <span class="text-sm" style="color: var(--theme-titulo);">{subpartida.desc_objeto}</span>
391 {#if subpartida.n_variaciones > 1} 1342 {#if subpartida.n_variaciones > 1}
392 <button 1343 <button
393 class="text-xs text-orange-500 hover:text-orange-700 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer" 1344 class="text-xs text-orange-500 hover:text-orange-700 transition-colors underline decoration-dotted decoration-orange-300 hover:decoration-orange-500 cursor-pointer"
...@@ -399,7 +1350,8 @@ ...@@ -399,7 +1350,8 @@
399 {/if} 1350 {/if}
400 <a 1351 <a
401 href="/objeto/{subpartida.objeto}" 1352 href="/objeto/{subpartida.objeto}"
402 - class="text-slate-300 hover:text-blue-500 transition-colors" 1353 + class="transition-colors hover:text-[var(--theme-accent)]"
1354 + style="color: var(--theme-texto);"
403 title="Ver página de detalle" 1355 title="Ver página de detalle"
404 > 1356 >
405 <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 1357 <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
...@@ -408,7 +1360,7 @@ ...@@ -408,7 +1360,7 @@
408 </a> 1360 </a>
409 </span> 1361 </span>
410 {#if subpartidaDescs.length > 0} 1362 {#if subpartidaDescs.length > 0}
411 - <p class="text-xs text-slate-400 mt-0.5">{subpartidaDescs[0].descripcion}</p> 1363 + <p class="text-sm mt-1" style="color: var(--theme-texto);">{subpartidaDescs[0].descripcion}</p>
412 {/if} 1364 {/if}
413 </div> 1365 </div>
414 </div> 1366 </div>
...@@ -428,33 +1380,35 @@ ...@@ -428,33 +1380,35 @@
428 </main> 1380 </main>
429 1381
430 <!-- Sidebar derecha: En esta página --> 1382 <!-- Sidebar derecha: En esta página -->
431 - <aside class="w-56 flex-shrink-0 hidden xl:block"> 1383 + <aside class="w-48 flex-shrink-0 hidden xl:block">
432 - <div class="sticky top-0 h-screen overflow-y-auto p-4 border-l"> 1384 + <div class="sticky top-0 h-screen overflow-y-auto py-8 pl-8 border-l" style="border-color: var(--theme-borde);">
433 - <p class="text-xs text-slate-500 uppercase tracking-wide mb-3">En esta página</p> 1385 + <p class="text-xs uppercase tracking-wide mb-3" style="color: var(--theme-texto);">En esta página</p>
434 {#if selectedGrupo && !isSearching} 1386 {#if selectedGrupo && !isSearching}
435 <nav class="space-y-2"> 1387 <nav class="space-y-2">
436 {#each grupoContent.subgrupos as subgrupo} 1388 {#each grupoContent.subgrupos as subgrupo}
437 <div> 1389 <div>
438 <a 1390 <a
439 href="#sg-{subgrupo.objeto}" 1391 href="#sg-{subgrupo.objeto}"
440 - class="block text-sm text-slate-600 hover:text-blue-600 truncate" 1392 + class="block text-sm truncate transition-colors hover:opacity-80"
1393 + style="color: var(--theme-titulo);"
441 title="{subgrupo.desc_objeto}" 1394 title="{subgrupo.desc_objeto}"
442 > 1395 >
443 {subgrupo.desc_objeto} 1396 {subgrupo.desc_objeto}
444 </a> 1397 </a>
445 {#if subgrupo.partidas?.length > 0} 1398 {#if subgrupo.partidas?.length > 0}
446 - <div class="ml-3 mt-1 space-y-1 border-l border-slate-100 pl-2"> 1399 + <div class="ml-3 mt-1 space-y-1 border-l pl-2" style="border-color: color-mix(in srgb, var(--theme-borde) 50%, transparent);">
447 {#each subgrupo.partidas.slice(0, 5) as partida} 1400 {#each subgrupo.partidas.slice(0, 5) as partida}
448 <a 1401 <a
449 href="#p-{partida.objeto}" 1402 href="#p-{partida.objeto}"
450 - class="block text-xs text-slate-400 hover:text-blue-600 truncate" 1403 + class="block text-xs truncate transition-colors hover:opacity-80"
1404 + style="color: var(--theme-texto);"
451 title="{partida.desc_objeto}" 1405 title="{partida.desc_objeto}"
452 > 1406 >
453 {partida.desc_objeto} 1407 {partida.desc_objeto}
454 </a> 1408 </a>
455 {/each} 1409 {/each}
456 {#if subgrupo.partidas.length > 5} 1410 {#if subgrupo.partidas.length > 5}
457 - <span class="text-xs text-slate-300">+{subgrupo.partidas.length - 5} más</span> 1411 + <span class="text-xs" style="color: var(--theme-texto);">+{subgrupo.partidas.length - 5} más</span>
458 {/if} 1412 {/if}
459 </div> 1413 </div>
460 {/if} 1414 {/if}
...@@ -465,6 +1419,487 @@ ...@@ -465,6 +1419,487 @@
465 </div> 1419 </div>
466 </aside> 1420 </aside>
467 </div> 1421 </div>
1422 +
1423 + {:else if viewMode === 'mapa'}
1424 + <!-- ═══════════════════════════════════════════════════════════ -->
1425 + <!-- MODO MAPA: Treemap de proporciones del gasto -->
1426 + <!-- ═══════════════════════════════════════════════════════════ -->
1427 +
1428 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 pt-3 pb-4">
1429 +
1430 + <!-- Breadcrumb de navegación (solo en modo jerárquico) -->
1431 + {#if treemapViewLevel === 'jerarquico' && treemapBreadcrumb.length > 0}
1432 + <nav class="flex items-center gap-2 mb-2 flex-wrap" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;">
1433 + {#each treemapBreadcrumb as crumb, i}
1434 + {#if i > 0}
1435 + <span style="color: var(--theme-texto); opacity: 0.5;">›</span>
1436 + {/if}
1437 + <button
1438 + onclick={() => drillUp(i)}
1439 + class="px-2 py-1 rounded transition-colors"
1440 + style="color: {i === treemapBreadcrumb.length - 1 ? 'var(--theme-titulo)' : 'var(--theme-texto)'}; background-color: {i === treemapBreadcrumb.length - 1 ? 'var(--theme-fill)' : 'transparent'}; cursor: pointer;"
1441 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--theme-fill)'}
1442 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = i === treemapBreadcrumb.length - 1 ? 'var(--theme-fill)' : 'transparent'}
1443 + >
1444 + {crumb.name}
1445 + </button>
1446 + {/each}
1447 + </nav>
1448 + {:else if treemapViewLevel !== 'jerarquico'}
1449 + <!-- Info de nivel aplanado -->
1450 + <div class="mb-2 flex items-center gap-3" style="font-family: 'DM Mono', monospace; font-size: 0.75rem;">
1451 + <span class="px-2 py-1 rounded" style="background-color: var(--theme-fill); color: var(--theme-titulo);">
1452 + {NIVEL_OPTIONS.find(o => o.value === treemapViewLevel)?.label || treemapViewLevel}
1453 + </span>
1454 + <span style="color: var(--theme-texto);">
1455 + {treemapNodes.filter(n => n.type === 'item').length} categorías en {treemapNodes.filter(n => n.type === 'grupo').length} grupos
1456 + </span>
1457 + </div>
1458 + {/if}
1459 +
1460 + <!-- Contenedor del Treemap -->
1461 + <div
1462 + bind:this={treemapContainer}
1463 + class="relative rounded-xl overflow-hidden w-full"
1464 + style="height: {treemapHeight}px;"
1465 + >
1466 + {#if treemapNodes.length > 0 && treemapWidth > 0}
1467 + <svg
1468 + width={treemapWidth}
1469 + height={treemapHeight}
1470 + class="block"
1471 + >
1472 + {#if treemapViewLevel === 'jerarquico'}
1473 + <!-- MODO JERÁRQUICO: con drill-down -->
1474 + {#each treemapNodes as node}
1475 + {@const width = node.x1 - node.x0}
1476 + {@const height = node.y1 - node.y0}
1477 + {@const hasChildren = node._originalNode?.children && node._originalNode.children.length > 0}
1478 + {@const isHovered = hoveredNode === node}
1479 + {@const textColor = getTextColor(node.id)}
1480 + {@const textColorSecondary = getTextColorSecondary(node.id)}
1481 + {@const label = node.data?.desc_objeto || node.name || node.id}
1482 +
1483 + <g
1484 + class="treemap-node"
1485 + transform="translate({node.x0}, {node.y0})"
1486 + onclick={() => hasChildren && drillDown(node)}
1487 + onmouseenter={() => hoveredNode = node}
1488 + onmouseleave={() => hoveredNode = null}
1489 + style="cursor: {hasChildren ? 'pointer' : 'default'};"
1490 + >
1491 + <rect
1492 + width={width}
1493 + height={height}
1494 + fill={getNodeColor(node.id, isHovered ? 1 : 0.85)}
1495 + stroke={isHovered ? 'var(--theme-titulo)' : 'var(--theme-surface)'}
1496 + stroke-width={isHovered ? 2 : 1}
1497 + rx="2"
1498 + />
1499 +
1500 + {#if width > 60 && height > 40}
1501 + <foreignObject x="6" y="6" width={width - 12} height={height - 12}>
1502 + <div class="h-full flex flex-col justify-between overflow-hidden" style="font-family: 'Instrument Sans', sans-serif;">
1503 + <div class="font-medium leading-tight" style="font-size: {Math.min(14, Math.max(10, width / 12))}px; color: {textColor};">
1504 + {#if width > 100}
1505 + {label}
1506 + {:else}
1507 + {label.slice(0, 20)}{label.length > 20 ? '…' : ''}
1508 + {/if}
1509 + </div>
1510 + {#if height > 55}
1511 + <div style="font-family: 'DM Mono', monospace; font-size: {Math.min(12, Math.max(9, width / 14))}px; color: {textColorSecondary};">
1512 + {formatMoney(node.value)}
1513 + {#if width > 90 && height > 70}
1514 + <span style="opacity: 0.7;"> · {formatPerCapita(node.value)}</span>
1515 + {/if}
1516 + </div>
1517 + {/if}
1518 + </div>
1519 + </foreignObject>
1520 + {/if}
1521 +
1522 + {#if hasChildren && width > 30 && height > 30}
1523 + <circle
1524 + cx={width - 12}
1525 + cy={12}
1526 + r="8"
1527 + fill={textColor.includes('255') ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.15)'}
1528 + class="transition-opacity"
1529 + style="opacity: {isHovered ? 1 : 0.5};"
1530 + />
1531 + <text
1532 + x={width - 12}
1533 + y={16}
1534 + text-anchor="middle"
1535 + fill={textColor}
1536 + font-size="10"
1537 + font-weight="bold"
1538 + >+</text>
1539 + {/if}
1540 + </g>
1541 + {/each}
1542 + {:else}
1543 + <!-- MODO APLANADO: grupos con títulos + items -->
1544 + {#each treemapNodes as node}
1545 + {@const width = node.x1 - node.x0}
1546 + {@const height = node.y1 - node.y0}
1547 + {@const isHovered = hoveredNode === node}
1548 + {@const grupoId = node.grupoCode + '0000'}
1549 + {@const textColor = getTextColor(grupoId)}
1550 + {@const textColorSecondary = getTextColorSecondary(grupoId)}
1551 +
1552 + {#if node.type === 'grupo'}
1553 + <!-- Contenedor de grupo con título -->
1554 + <g class="treemap-node" transform="translate({node.x0}, {node.y0})">
1555 + <!-- Fondo del grupo (sutil) -->
1556 + <rect
1557 + width={width}
1558 + height={height}
1559 + fill={getNodeColor(grupoId, 0.08)}
1560 + stroke={getNodeColor(grupoId, 0.3)}
1561 + stroke-width="1"
1562 + rx="3"
1563 + />
1564 + <!-- Título del grupo -->
1565 + {#if width > 50}
1566 + <text
1567 + x="6"
1568 + y="14"
1569 + fill={getNodeColor(grupoId, 1)}
1570 + font-size="10"
1571 + font-weight="600"
1572 + style="font-family: 'DM Mono', monospace; text-transform: uppercase; letter-spacing: 0.5px;"
1573 + >
1574 + {#if width > 180}
1575 + {node.name}
1576 + {:else if width > 100}
1577 + {node.name.slice(0, 18)}{node.name.length > 18 ? '…' : ''}
1578 + {:else if width > 60}
1579 + {node.name.slice(0, 10)}…
1580 + {:else}
1581 + G{node.grupoCode}
1582 + {/if}
1583 + </text>
1584 + {/if}
1585 + </g>
1586 + {:else}
1587 + <!-- Item individual -->
1588 + <g
1589 + class="treemap-node"
1590 + transform="translate({node.x0}, {node.y0})"
1591 + onmouseenter={() => hoveredNode = node}
1592 + onmouseleave={() => hoveredNode = null}
1593 + style="cursor: pointer;"
1594 + >
1595 + <rect
1596 + width={width}
1597 + height={height}
1598 + fill={getNodeColor(grupoId, isHovered ? 1 : 0.85)}
1599 + stroke={isHovered ? 'var(--theme-titulo)' : 'transparent'}
1600 + stroke-width={isHovered ? 2 : 0}
1601 + rx="2"
1602 + />
1603 +
1604 + {#if width > 40 && height > 25}
1605 + <foreignObject x="4" y="4" width={width - 8} height={height - 8}>
1606 + <div class="h-full flex flex-col justify-between overflow-hidden" style="font-family: 'Instrument Sans', sans-serif;">
1607 + <div class="font-medium leading-tight" style="font-size: {Math.min(12, Math.max(9, width / 14))}px; color: {textColor};">
1608 + {#if width > 80}
1609 + {(node.name || node.id).slice(0, Math.floor(width / 6))}{(node.name || node.id).length > Math.floor(width / 6) ? '…' : ''}
1610 + {:else}
1611 + {node.id}
1612 + {/if}
1613 + </div>
1614 + {#if height > 40 && width > 50}
1615 + <div style="font-family: 'DM Mono', monospace; font-size: {Math.min(10, Math.max(8, width / 16))}px; color: {textColorSecondary};">
1616 + {formatMoney(node.value)}
1617 + {#if width > 80 && height > 55}
1618 + <span style="opacity: 0.7;"> · {formatPerCapita(node.value)}</span>
1619 + {/if}
1620 + </div>
1621 + {/if}
1622 + </div>
1623 + </foreignObject>
1624 + {/if}
1625 + </g>
1626 + {/if}
1627 + {/each}
1628 + {/if}
1629 + </svg>
1630 +
1631 + <!-- Tooltip flotante -->
1632 + {#if hoveredNode && hoveredNode.type !== 'grupo'}
1633 + {@const totalValue = currentTreemapNode?.value || treemapRoot?.value || 1}
1634 + <div
1635 + class="absolute pointer-events-none px-3 py-2 rounded-lg shadow-lg z-10 max-w-xs"
1636 + style="
1637 + left: {Math.min(hoveredNode.x0 + 10, treemapWidth - 220)}px;
1638 + top: {Math.min(hoveredNode.y0 + 10, treemapHeight - 100)}px;
1639 + background-color: var(--theme-titulo);
1640 + color: var(--theme-body);
1641 + font-family: 'Instrument Sans', sans-serif;
1642 + "
1643 + >
1644 + {#if treemapViewLevel !== 'jerarquico'}
1645 + <!-- Vista aplanada: mostrar contexto del grupo -->
1646 + <div class="text-xs opacity-60 mb-1" style="font-family: 'DM Mono', monospace;">
1647 + {hoveredNode.id}
1648 + </div>
1649 + <div class="font-medium text-sm">{hoveredNode.name || hoveredNode.id}</div>
1650 + <div class="text-xs opacity-80 mt-1" style="font-family: 'DM Mono', monospace;">
1651 + {formatMoney(hoveredNode.value)} · {((hoveredNode.value / totalValue) * 100).toFixed(1)}%
1652 + </div>
1653 + <div class="text-xs opacity-60 mt-0.5" style="font-family: 'DM Mono', monospace;">
1654 + {formatPerCapita(hoveredNode.value)}
1655 + </div>
1656 + {:else}
1657 + <!-- Vista jerárquica -->
1658 + <div class="font-medium text-sm">{hoveredNode.data?.desc_objeto || hoveredNode.name || hoveredNode.id}</div>
1659 + <div class="text-xs opacity-80 mt-1" style="font-family: 'DM Mono', monospace;">
1660 + {formatMoney(hoveredNode.value)}
1661 + {#if currentTreemapNode && currentTreemapNode.value}
1662 + · {((hoveredNode.value / currentTreemapNode.value) * 100).toFixed(1)}%
1663 + {/if}
1664 + </div>
1665 + <div class="text-xs opacity-60 mt-0.5" style="font-family: 'DM Mono', monospace;">
1666 + {formatPerCapita(hoveredNode.value)}
1667 + </div>
1668 + {#if hoveredNode._originalNode?.children && hoveredNode._originalNode.children.length > 0}
1669 + <div class="text-xs opacity-60 mt-1">Clic para explorar</div>
1670 + {/if}
1671 + {/if}
1672 + </div>
1673 + {/if}
1674 + {:else}
1675 + <!-- Estado vacío / cargando -->
1676 + <div class="absolute inset-0 flex flex-col items-center justify-center p-8">
1677 + <svg class="w-12 h-12 mb-4 animate-pulse" style="color: var(--theme-texto); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1678 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
1679 + </svg>
1680 + <p class="text-sm" style="color: var(--theme-texto);">Cargando visualización...</p>
1681 + </div>
1682 + {/if}
1683 + </div>
1684 +
1685 + </div>
1686 +
1687 + {:else if viewMode === 'comparar'}
1688 + <!-- ═══════════════════════════════════════════════════════════ -->
1689 + <!-- MODO COMPARAR: Dos treemaps lado a lado -->
1690 + <!-- ═══════════════════════════════════════════════════════════ -->
1691 +
1692 + <div class="max-w-screen-xl mx-auto px-4 sm:px-6 py-8">
1693 + <!-- Selectores de comparación -->
1694 + <div class="flex flex-col lg:flex-row gap-6 lg:gap-8 mb-8">
1695 + <!-- Lado A -->
1696 + <div class="flex-1">
1697 + <div class="flex items-center gap-3 mb-4">
1698 + <span class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm" style="background-color: color-mix(in srgb, var(--theme-accent) 20%, transparent); color: var(--theme-accent);">A</span>
1699 + <div class="flex flex-wrap items-center gap-2">
1700 + <!-- Año A -->
1701 + <select
1702 + bind:value={compareA.year}
1703 + class="px-3 py-2 text-sm rounded-lg focus:outline-none focus:ring-2 cursor-pointer font-medium"
1704 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
1705 + >
1706 + {#each availableYears as year}
1707 + <option value={year}>{year}</option>
1708 + {/each}
1709 + </select>
1710 +
1711 + <!-- Entidad A -->
1712 + <div class="relative compare-a-dropdown">
1713 + <button
1714 + class="px-3 py-2 text-sm rounded-lg focus:outline-none focus:ring-2 min-w-[200px] text-left flex items-center justify-between gap-2"
1715 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
1716 + onclick={() => compareADropdownOpen = !compareADropdownOpen}
1717 + >
1718 + <span class="truncate font-medium">
1719 + {#if compareA.entity}
1720 + {compareA.entity.sigla_entidad || compareA.entity.desc_entidad?.slice(0, 25)}
1721 + {:else}
1722 + Todo el Estado
1723 + {/if}
1724 + </span>
1725 + <svg class="w-4 h-4 shrink-0" style="color: var(--theme-texto);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1726 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1727 + </svg>
1728 + </button>
1729 +
1730 + {#if compareADropdownOpen}
1731 + <div class="absolute top-full left-0 mt-1 w-80 rounded-lg shadow-lg z-50 max-h-72 overflow-hidden" style="background-color: var(--theme-surface); border: 1px solid var(--theme-borde);">
1732 + <div class="p-2 border-b" style="border-color: var(--theme-borde);">
1733 + <input
1734 + type="text"
1735 + bind:value={compareASearchQuery}
1736 + placeholder="Buscar entidad..."
1737 + class="w-full px-3 py-1.5 text-sm rounded focus:outline-none focus:ring-2"
1738 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-fill); color: var(--theme-titulo);"
1739 + />
1740 + </div>
1741 + <button
1742 + class="w-full text-left px-3 py-2 text-sm border-b"
1743 + style="{!compareA.entity ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);` : ``} color: var(--theme-titulo); border-color: var(--theme-borde);"
1744 + onclick={() => clearCompareEntity('A')}
1745 + >
1746 + <span class="font-medium">Todo el Estado</span>
1747 + </button>
1748 + <div class="overflow-y-auto max-h-48">
1749 + {#each filteredEntitiesA() as entity}
1750 + <button
1751 + class="w-full text-left px-3 py-2 text-sm"
1752 + style="{compareA.entity?.entidad === entity.entidad ? `background-color: color-mix(in srgb, var(--theme-accent) 15%, transparent);` : ``} color: var(--theme-titulo);"
1753 + onclick={() => selectCompareEntity('A', entity)}
1754 + >
1755 + <div class="flex items-center gap-2">
1756 + <span class="font-mono text-xs" style="color: var(--theme-texto);">{entity.entidad}</span>
1757 + <span class="truncate">{entity.desc_entidad}</span>
1758 + </div>
1759 + </button>
1760 + {/each}
1761 + </div>
1762 + </div>
1763 + {/if}
1764 + </div>
1765 + </div>
1766 + </div>
1767 +
1768 + <!-- Treemap A placeholder -->
1769 + <div class="border-2 border-dashed rounded-xl min-h-[350px] flex flex-col items-center justify-center p-6" style="background-color: color-mix(in srgb, var(--theme-accent) 5%, transparent); border-color: color-mix(in srgb, var(--theme-accent) 30%, transparent);">
1770 + <svg class="w-12 h-12 mb-3" style="color: var(--theme-accent); opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1771 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
1772 + </svg>
1773 + <p class="font-medium" style="color: var(--theme-accent);">Treemap A</p>
1774 + <p class="text-sm mt-1" style="color: var(--theme-texto);">
1775 + {compareA.year} · {compareA.entity?.sigla_entidad || 'Todo el Estado'}
1776 + </p>
1777 + </div>
1778 + </div>
1779 +
1780 + <!-- Botón de intercambio (centro) -->
1781 + <div class="flex lg:flex-col items-center justify-center gap-2">
1782 + <button
1783 + onclick={swapCompareSides}
1784 + class="p-2 rounded-full border transition-colors"
1785 + style="border-color: var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-texto);"
1786 + title="Intercambiar lados"
1787 + >
1788 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1789 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
1790 + </svg>
1791 + </button>
1792 + <span class="font-medium text-sm" style="color: var(--theme-texto);">vs</span>
1793 + </div>
1794 +
1795 + <!-- Lado B -->
1796 + <div class="flex-1">
1797 + <div class="flex items-center gap-3 mb-4">
1798 + <span class="w-8 h-8 rounded-full flex items-center justify-center font-semibold text-sm" style="background-color: color-mix(in srgb, #8b5cf6 20%, transparent); color: #8b5cf6;">B</span>
1799 + <div class="flex flex-wrap items-center gap-2">
1800 + <!-- Año B -->
1801 + <select
1802 + bind:value={compareB.year}
1803 + class="px-3 py-2 text-sm rounded-lg focus:outline-none focus:ring-2 cursor-pointer font-medium"
1804 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
1805 + >
1806 + {#each availableYears as year}
1807 + <option value={year}>{year}</option>
1808 + {/each}
1809 + </select>
1810 +
1811 + <!-- Entidad B -->
1812 + <div class="relative compare-b-dropdown">
1813 + <button
1814 + class="px-3 py-2 text-sm rounded-lg focus:outline-none focus:ring-2 min-w-[200px] text-left flex items-center justify-between gap-2"
1815 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-surface); color: var(--theme-titulo);"
1816 + onclick={() => compareBDropdownOpen = !compareBDropdownOpen}
1817 + >
1818 + <span class="truncate font-medium">
1819 + {#if compareB.entity}
1820 + {compareB.entity.sigla_entidad || compareB.entity.desc_entidad?.slice(0, 25)}
1821 + {:else}
1822 + Todo el Estado
1823 + {/if}
1824 + </span>
1825 + <svg class="w-4 h-4 shrink-0" style="color: var(--theme-texto);" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1826 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
1827 + </svg>
1828 + </button>
1829 +
1830 + {#if compareBDropdownOpen}
1831 + <div class="absolute top-full left-0 mt-1 w-80 rounded-lg shadow-lg z-50 max-h-72 overflow-hidden" style="background-color: var(--theme-surface); border: 1px solid var(--theme-borde);">
1832 + <div class="p-2 border-b" style="border-color: var(--theme-borde);">
1833 + <input
1834 + type="text"
1835 + bind:value={compareBSearchQuery}
1836 + placeholder="Buscar entidad..."
1837 + class="w-full px-3 py-1.5 text-sm rounded focus:outline-none focus:ring-2"
1838 + style="border: 1px solid var(--theme-borde); background-color: var(--theme-fill); color: var(--theme-titulo);"
1839 + />
1840 + </div>
1841 + <button
1842 + class="w-full text-left px-3 py-2 text-sm border-b"
1843 + style="{!compareB.entity ? `background-color: color-mix(in srgb, #8b5cf6 15%, transparent);` : ``} color: var(--theme-titulo); border-color: var(--theme-borde);"
1844 + onclick={() => clearCompareEntity('B')}
1845 + >
1846 + <span class="font-medium">Todo el Estado</span>
1847 + </button>
1848 + <div class="overflow-y-auto max-h-48">
1849 + {#each filteredEntitiesB() as entity}
1850 + <button
1851 + class="w-full text-left px-3 py-2 text-sm"
1852 + style="{compareB.entity?.entidad === entity.entidad ? `background-color: color-mix(in srgb, #8b5cf6 15%, transparent);` : ``} color: var(--theme-titulo);"
1853 + onclick={() => selectCompareEntity('B', entity)}
1854 + >
1855 + <div class="flex items-center gap-2">
1856 + <span class="font-mono text-xs" style="color: var(--theme-texto);">{entity.entidad}</span>
1857 + <span class="truncate">{entity.desc_entidad}</span>
1858 + </div>
1859 + </button>
1860 + {/each}
1861 + </div>
1862 + </div>
1863 + {/if}
1864 + </div>
1865 + </div>
1866 + </div>
1867 +
1868 + <!-- Treemap B placeholder -->
1869 + <div class="border-2 border-dashed rounded-xl min-h-[350px] flex flex-col items-center justify-center p-6" style="background-color: color-mix(in srgb, #8b5cf6 5%, transparent); border-color: color-mix(in srgb, #8b5cf6 30%, transparent);">
1870 + <svg class="w-12 h-12 mb-3" style="color: #8b5cf6; opacity: 0.5;" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1871 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM14 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zM14 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
1872 + </svg>
1873 + <p class="font-medium" style="color: #8b5cf6;">Treemap B</p>
1874 + <p class="text-sm mt-1" style="color: var(--theme-texto);">
1875 + {compareB.year} · {compareB.entity?.sigla_entidad || 'Todo el Estado'}
1876 + </p>
1877 + </div>
1878 + </div>
1879 + </div>
1880 +
1881 + <!-- Resumen de diferencias (placeholder) -->
1882 + <div class="border rounded-xl p-6" style="background-color: var(--theme-fill); border-color: var(--theme-borde);">
1883 + <h3 class="text-sm font-medium uppercase tracking-wide mb-4" style="font-family: 'DM Mono', monospace; color: var(--theme-titulo);">
1884 + Diferencias principales
1885 + </h3>
1886 + <div class="grid sm:grid-cols-3 gap-4 text-center">
1887 + <div class="p-4 rounded-lg border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">
1888 + <p class="text-xs uppercase tracking-wide mb-1" style="color: var(--theme-texto);">Mayor crecimiento</p>
1889 + <p class="text-sm italic" style="color: var(--theme-texto);">Pendiente de datos</p>
1890 + </div>
1891 + <div class="p-4 rounded-lg border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">
1892 + <p class="text-xs uppercase tracking-wide mb-1" style="color: var(--theme-texto);">Mayor reducción</p>
1893 + <p class="text-sm italic" style="color: var(--theme-texto);">Pendiente de datos</p>
1894 + </div>
1895 + <div class="p-4 rounded-lg border" style="background-color: var(--theme-surface); border-color: var(--theme-borde);">
1896 + <p class="text-xs uppercase tracking-wide mb-1" style="color: var(--theme-texto);">Cambio total</p>
1897 + <p class="text-sm italic" style="color: var(--theme-texto);">Pendiente de datos</p>
1898 + </div>
1899 + </div>
1900 + </div>
1901 + </div>
1902 + {/if}
468 {/if} 1903 {/if}
469 </div> 1904 </div>
470 1905
...@@ -472,28 +1907,30 @@ ...@@ -472,28 +1907,30 @@
472 {#if selectedItem} 1907 {#if selectedItem}
473 <div class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" onclick={closeDetail}> 1908 <div class="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" onclick={closeDetail}>
474 <div 1909 <div
475 - class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col" 1910 + class="rounded-xl shadow-2xl max-w-2xl w-full max-h-[85vh] overflow-hidden flex flex-col"
1911 + style="background-color: var(--theme-surface);"
476 onclick={(e) => e.stopPropagation()} 1912 onclick={(e) => e.stopPropagation()}
477 > 1913 >
478 - <div class="px-6 py-4 border-b bg-slate-50 flex justify-between items-start"> 1914 + <div class="px-6 py-4 border-b flex justify-between items-start" style="background-color: var(--theme-fill); border-color: var(--theme-borde);">
479 <div> 1915 <div>
480 - <p class="text-xs text-slate-500 uppercase tracking-wide">{getNivelLabel(selectedItem.nivel)}</p> 1916 + <p class="text-xs uppercase tracking-wide font-medium" style="color: var(--theme-texto);">{getNivelLabel(selectedItem.nivel)}</p>
481 - <h3 class="text-lg font-medium text-slate-900 mt-1"> 1917 + <h3 class="text-lg font-medium mt-1" style="color: var(--theme-titulo);">
482 - <span class="font-mono text-slate-400">{selectedItem.objeto}</span> 1918 + <span class="font-mono" style="color: var(--theme-texto);">{selectedItem.objeto}</span>
483 - <span class="mx-2">·</span> 1919 + <span class="mx-2" style="color: var(--theme-texto); opacity: 0.5;">·</span>
484 {selectedItem.desc_objeto} 1920 {selectedItem.desc_objeto}
485 </h3> 1921 </h3>
486 </div> 1922 </div>
487 <button 1923 <button
488 onclick={closeDetail} 1924 onclick={closeDetail}
489 - class="text-slate-400 hover:text-slate-600 text-2xl leading-none" 1925 + class="text-2xl leading-none"
1926 + style="color: var(--theme-texto);"
490 > 1927 >
491 &times; 1928 &times;
492 </button> 1929 </button>
493 </div> 1930 </div>
494 1931
495 <div class="p-6 overflow-y-auto flex-1"> 1932 <div class="p-6 overflow-y-auto flex-1">
496 - <h4 class="text-sm font-medium text-slate-700 mb-4"> 1933 + <h4 class="text-sm font-medium mb-4" style="color: var(--theme-titulo);">
497 {#if selectedItem.n_variaciones > 1} 1934 {#if selectedItem.n_variaciones > 1}
498 Descripciones ({selectedItem.n_variaciones} variaciones) 1935 Descripciones ({selectedItem.n_variaciones} variaciones)
499 {:else} 1936 {:else}
...@@ -503,15 +1940,15 @@ ...@@ -503,15 +1940,15 @@
503 1940
504 <div class="space-y-4"> 1941 <div class="space-y-4">
505 {#each parseDescripciones(selectedItem.descripciones) as desc, i} 1942 {#each parseDescripciones(selectedItem.descripciones) as desc, i}
506 - <div class="border-l-2 {i === 0 ? 'border-blue-500 bg-blue-50/50' : 'border-slate-200'} pl-4 py-2 rounded-r"> 1943 + <div class="border-l-2 pl-4 py-3 rounded-r" style="{i === 0 ? `border-color: var(--theme-accent); background-color: color-mix(in srgb, var(--theme-accent) 10%, transparent);` : `border-color: var(--theme-borde);`}">
507 - <p class="text-xs text-slate-500 mb-2"> 1944 + <p class="text-sm mb-2" style="color: var(--theme-texto);">
508 {#if i === 0 && selectedItem.n_variaciones > 1} 1945 {#if i === 0 && selectedItem.n_variaciones > 1}
509 - <span class="text-blue-600 font-medium">Vigente</span> 1946 + <span class="font-medium" style="color: var(--theme-accent);">Vigente</span>
510 - <span class="mx-1">·</span> 1947 + <span class="mx-1" style="opacity: 0.5;">·</span>
511 {/if} 1948 {/if}
512 - {desc.rangos} 1949 + <span class="font-mono">{desc.rangos}</span>
513 </p> 1950 </p>
514 - <p class="text-sm text-slate-700 leading-relaxed">{desc.descripcion}</p> 1951 + <p class="text-base leading-relaxed" style="color: var(--theme-titulo);">{desc.descripcion}</p>
515 </div> 1952 </div>
516 {/each} 1953 {/each}
517 </div> 1954 </div>
...@@ -519,3 +1956,29 @@ ...@@ -519,3 +1956,29 @@
519 </div> 1956 </div>
520 </div> 1957 </div>
521 {/if} 1958 {/if}
1959 +
1960 +<style>
1961 + /* Sidebar izquierdo: con fondo en móvil, transparente en desktop */
1962 + .sidebar-left {
1963 + background-color: var(--theme-surface);
1964 + }
1965 + @media (min-width: 1024px) {
1966 + .sidebar-left {
1967 + background-color: transparent;
1968 + }
1969 + }
1970 +
1971 + /* Transiciones suaves para el treemap */
1972 + :global(.treemap-node) {
1973 + transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
1974 + }
1975 + :global(.treemap-node rect) {
1976 + transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1),
1977 + height 0.5s cubic-bezier(0.4, 0, 0.2, 1),
1978 + fill 0.3s ease;
1979 + }
1980 + :global(.treemap-fade-enter) {
1981 + opacity: 0;
1982 + transform: scale(0.95);
1983 + }
1984 +</style>
......
1 +# Estilos UI - Dark Mode, Menu y Drawer
2 +
3 +## 1. Variables CSS (app.css)
4 +
5 +```css
6 +/* Light mode colors */
7 +:root {
8 + --color-light-body: #f5ebe0;
9 + --color-light-background: #f3ead8;
10 + --color-light-titulo: #363534;
11 + --color-light-texto-auxiliar: #676767;
12 + --color-light-fill: #f3ead8;
13 + --color-light-borde: rgba(0, 0, 0, 0.08);
14 + --color-light-focus-secondary: #676767;
15 + --color-light-accent: #c9a751;
16 +}
17 +
18 +/* Dark mode colors */
19 +:root.dark {
20 + --color-light-body: #0d0d0d;
21 + --color-light-background: #1a1a1a;
22 + --color-light-titulo: #e8e4d8;
23 + --color-light-texto-auxiliar: #b8b3a3;
24 + --color-light-fill: #2d2d2d;
25 + --color-light-borde: rgba(255, 255, 255, 0.1);
26 + --color-light-focus-secondary: #c9a751;
27 + --color-light-accent: #c9a751;
28 +}
29 +```
30 +
31 +---
32 +
33 +## 2. Inicialización del tema (app.html)
34 +
35 +Evita el flash de tema incorrecto cargando la preferencia antes del render.
36 +
37 +```html
38 +<!doctype html>
39 +<html lang="es">
40 + <head>
41 + <meta charset="utf-8" />
42 + <meta name="viewport" content="width=device-width, initial-scale=1" />
43 +
44 + <!-- Theme initialization - ANTES de cargar CSS -->
45 + <script>
46 + (function() {
47 + const savedTheme = localStorage.getItem('theme');
48 + let isDark = false;
49 +
50 + if (savedTheme) {
51 + isDark = savedTheme === 'dark';
52 + }
53 +
54 + if (isDark) {
55 + document.documentElement.classList.add('dark');
56 + }
57 + })();
58 + </script>
59 + </head>
60 + <body>
61 + <!-- contenido -->
62 + </body>
63 +</html>
64 +```
65 +
66 +---
67 +
68 +## 3. BARRA DE CONTROLES (Pastilla con iconos)
69 +
70 +Esta es la barra que contiene: **Home + Theme Toggle + Menú Hamburguesa**
71 +
72 +### Estructura completa
73 +
74 +```svelte
75 +<script>
76 + import { onMount } from 'svelte';
77 +
78 + let isDark = $state(false);
79 + let drawerOpen = $state(false);
80 +
81 + function toggleTheme() {
82 + isDark = !isDark;
83 + document.documentElement.classList.toggle('dark', isDark);
84 + localStorage.setItem('theme', isDark ? 'dark' : 'light');
85 + }
86 +
87 + function toggleDrawer() {
88 + drawerOpen = !drawerOpen;
89 + }
90 +
91 + onMount(() => {
92 + const savedTheme = localStorage.getItem('theme');
93 + if (savedTheme) {
94 + isDark = savedTheme === 'dark';
95 + }
96 + document.documentElement.classList.toggle('dark', isDark);
97 + });
98 +</script>
99 +
100 +<!-- BARRA DE CONTROLES - Esquina superior derecha -->
101 +<div class="fixed top-6 right-6 z-[110]">
102 + <div class="flex items-center gap-1">
103 +
104 + <!-- ========== BOTÓN HOME (4 cuadrados) ========== -->
105 + <div class="relative group/home">
106 + <a
107 + href="/"
108 + class="p-2 rounded-full transition-all block"
109 + style="background-color: transparent; color: var(--color-light-texto-auxiliar);"
110 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--color-light-titulo)'}
111 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--color-light-texto-auxiliar)'}
112 + >
113 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
114 + <rect x="2" y="2" width="7" height="7" rx="1.5"/>
115 + <rect x="11" y="2" width="7" height="7" rx="1.5"/>
116 + <rect x="2" y="11" width="7" height="7" rx="1.5"/>
117 + <rect x="11" y="11" width="7" height="7" rx="1.5"/>
118 + </svg>
119 + </a>
120 + <!-- Tooltip -->
121 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/home:opacity-100 group-hover/home:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
122 + Página principal
123 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
124 + </div>
125 + </div>
126 +
127 + <!-- ========== BOTÓN THEME TOGGLE (Sol/Luna) ========== -->
128 + <div class="relative group/theme">
129 + <button
130 + onclick={toggleTheme}
131 + class="p-2 rounded-full transition-all"
132 + style="background-color: transparent; color: var(--color-light-texto-auxiliar); cursor: pointer;"
133 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--color-light-titulo)'}
134 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--color-light-texto-auxiliar)'}
135 + aria-label={isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
136 + >
137 + {#if isDark}
138 + <!-- LUNA (modo oscuro activo) -->
139 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
140 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
141 + </svg>
142 + {:else}
143 + <!-- SOL (modo claro activo) -->
144 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
145 + <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
146 + </svg>
147 + {/if}
148 + </button>
149 + <!-- Tooltip -->
150 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/theme:opacity-100 group-hover/theme:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
151 + {isDark ? 'Cambiar a modo claro' : 'Cambiar a modo oscuro'}
152 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
153 + </div>
154 + </div>
155 +
156 + <!-- ========== BOTÓN MENÚ HAMBURGUESA ========== -->
157 + <div class="relative group/menu">
158 + <button
159 + onclick={toggleDrawer}
160 + class="p-2 rounded-full transition-all"
161 + style="background-color: transparent; color: var(--color-light-texto-auxiliar); cursor: pointer;"
162 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--color-light-titulo)'}
163 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--color-light-texto-auxiliar)'}
164 + aria-label="Abrir menú"
165 + >
166 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
167 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
168 + </svg>
169 + </button>
170 + <!-- Tooltip -->
171 + <div class="absolute right-0 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover/menu:opacity-100 group-hover/menu:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
172 + Menú
173 + <div class="absolute right-3 bottom-full mb-[-4px] w-2 h-2 bg-gray-900/90 rotate-45"></div>
174 + </div>
175 + </div>
176 +
177 + </div>
178 +</div>
179 +```
180 +
181 +### Estilos clave de cada botón
182 +
183 +| Propiedad | Valor |
184 +|-----------|-------|
185 +| Padding | `p-2` (8px) |
186 +| Forma | `rounded-full` (circular) |
187 +| Fondo | `transparent` |
188 +| Color normal | `var(--color-light-texto-auxiliar)` |
189 +| Color hover | `var(--color-light-titulo)` |
190 +| Tamaño icono | `w-5 h-5` (20px) |
191 +| Transición | `transition-all` |
192 +| Cursor | `cursor: pointer` |
193 +
194 +### Estilos del contenedor (la "pastilla")
195 +
196 +| Propiedad | Valor |
197 +|-----------|-------|
198 +| Layout | `flex items-center` |
199 +| Espaciado | `gap-1` (4px entre iconos) |
200 +| Posición | `fixed top-6 right-6` |
201 +| Z-index | `z-[110]` |
202 +
203 +### Tooltip (aparece abajo del icono)
204 +
205 +| Propiedad | Valor |
206 +|-----------|-------|
207 +| Posición | `absolute right-0 top-full mt-2` |
208 +| Fondo | `bg-gray-900/90` |
209 +| Texto | `text-white text-xs` |
210 +| Padding | `px-3 py-1.5` |
211 +| Bordes | `rounded-lg` |
212 +| Efecto | `backdrop-blur-sm` |
213 +| Transición | `opacity-0 invisible``opacity-100 visible` en hover |
214 +| Flecha | `w-2 h-2 bg-gray-900/90 rotate-45` posicionada arriba |
215 +
216 +---
217 +
218 +## 4. VERSIÓN MÓVIL (Solo hamburguesa + theme en drawer)
219 +
220 +En móvil, solo se muestra el botón hamburguesa. El theme toggle va dentro del drawer.
221 +
222 +```svelte
223 +<!-- Móvil: solo hamburguesa arriba a la derecha -->
224 +<div class="fixed top-2 right-2 z-[110] md:hidden">
225 + <button
226 + onclick={toggleDrawer}
227 + class="p-2 rounded-full transition-all flex-shrink-0"
228 + style="background-color: transparent; color: var(--color-light-texto-auxiliar); cursor: pointer;"
229 + onmouseenter={(e) => e.currentTarget.style.color = 'var(--color-light-titulo)'}
230 + onmouseleave={(e) => e.currentTarget.style.color = 'var(--color-light-texto-auxiliar)'}
231 + aria-label="Abrir menú"
232 + >
233 + <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
234 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
235 + </svg>
236 + </button>
237 +</div>
238 +
239 +<!-- Desktop: barra completa con home + theme + menu -->
240 +<div class="hidden md:flex fixed top-6 right-6 z-[110]">
241 + <!-- ... los 3 botones ... -->
242 +</div>
243 +```
244 +
245 +---
246 +
247 +## 5. NavigationDrawer.svelte (Drawer completo)
248 +
249 +```svelte
250 +<script>
251 + import { onMount } from 'svelte';
252 +
253 + let { isOpen = $bindable(false) } = $props();
254 + let isDarkMode = $state(false);
255 +
256 + // Detectar cambios de tema
257 + onMount(() => {
258 + isDarkMode = document.documentElement.classList.contains('dark');
259 +
260 + const observer = new MutationObserver(() => {
261 + isDarkMode = document.documentElement.classList.contains('dark');
262 + });
263 + observer.observe(document.documentElement, {
264 + attributes: true,
265 + attributeFilter: ['class']
266 + });
267 + });
268 +
269 + function closeDrawer() {
270 + isOpen = false;
271 + }
272 +
273 + function toggleTheme() {
274 + isDarkMode = !isDarkMode;
275 + document.documentElement.classList.toggle('dark', isDarkMode);
276 + localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
277 + }
278 +</script>
279 +
280 +<!-- Overlay (fondo oscuro) -->
281 +{#if isOpen}
282 + <div
283 + class="fixed inset-0 bg-black/40 z-[200]"
284 + style="opacity: {isOpen ? '1' : '0'}; transition: opacity 0.2s ease-out;"
285 + onclick={closeDrawer}
286 + ></div>
287 +{/if}
288 +
289 +<!-- Drawer -->
290 +<aside
291 + class="fixed top-0 right-0 h-full w-80 border-l shadow-2xl z-[201]"
292 + style="
293 + background-color: var(--color-light-body);
294 + border-color: rgba(0, 0, 0, 0.05);
295 + transform: {isOpen ? 'translate3d(0, 0, 0)' : 'translate3d(100%, 0, 0)'};
296 + transition: transform 0.3s cubic-bezier(0.33, 1, 0.68, 1);
297 + "
298 +>
299 + <div class="flex flex-col h-full">
300 +
301 + <!-- Header del Drawer -->
302 + <div class="py-3 md:py-4 px-4 md:px-6 border-b flex-shrink-0 relative" style="border-color: rgba(0, 0, 0, 0.05);">
303 +
304 + <!-- Theme Toggle - SOLO en móvil (arriba izquierda del drawer) -->
305 + <div class="absolute top-3 md:top-4 left-4 md:left-6 md:hidden">
306 + <button
307 + onclick={toggleTheme}
308 + class="p-2 rounded-full transition-all"
309 + style="background-color: transparent; color: var(--color-light-texto-auxiliar); cursor: pointer;"
310 + >
311 + {#if isDarkMode}
312 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
313 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
314 + </svg>
315 + {:else}
316 + <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
317 + <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
318 + </svg>
319 + {/if}
320 + </button>
321 + </div>
322 +
323 + <!-- Botón cerrar (X) - arriba derecha -->
324 + <div class="group absolute top-3 md:top-4 right-4 md:right-6">
325 + <button
326 + onclick={closeDrawer}
327 + class="p-2 rounded-lg transition-colors flex-shrink-0"
328 + style="color: var(--color-light-focus-secondary); cursor: pointer;"
329 + aria-label="Cerrar menú"
330 + >
331 + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
332 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
333 + </svg>
334 + </button>
335 + <!-- Tooltip del botón cerrar -->
336 + <div class="absolute left-1/2 -translate-x-1/2 top-full mt-2 px-3 py-1.5 bg-gray-900/90 text-white text-xs rounded-lg whitespace-nowrap opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 pointer-events-none backdrop-blur-sm">
337 + Cerrar menú
338 + <div class="absolute left-1/2 -translate-x-1/2 -top-1 w-2 h-2 bg-gray-900/90 rotate-45"></div>
339 + </div>
340 + </div>
341 +
342 + <!-- Logo centrado (cambia según tema) -->
343 + <div class="flex flex-col items-center">
344 + <img
345 + src={isDarkMode ? "/logos_oscuro/logo.png" : "/logos_claro/logo.png"}
346 + alt="Logo"
347 + class="h-24 md:h-32 w-auto mb-3"
348 + />
349 + </div>
350 + </div>
351 +
352 + <!-- Navigation -->
353 + <nav class="flex-1 overflow-y-auto p-4">
354 +
355 + <!-- Item de navegación con hover -->
356 + <a
357 + href="/"
358 + class="w-full flex items-center gap-3 px-3 py-3 rounded-lg transition-colors text-left group"
359 + onmouseenter={(e) => e.currentTarget.style.backgroundColor = 'var(--color-light-fill)'}
360 + onmouseleave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
361 + >
362 + <svg class="w-4 h-4 flex-shrink-0" style="color: var(--color-light-focus-secondary);" fill="currentColor" viewBox="0 0 20 20">
363 + <rect x="2" y="2" width="7" height="7" rx="1.5"/>
364 + <rect x="11" y="2" width="7" height="7" rx="1.5"/>
365 + <rect x="2" y="11" width="7" height="7" rx="1.5"/>
366 + <rect x="11" y="11" width="7" height="7" rx="1.5"/>
367 + </svg>
368 + <span class="font-medium text-sm" style="color: var(--color-light-titulo);">Página principal</span>
369 + </a>
370 +
371 + <!-- Sección con título -->
372 + <div class="mb-6 mt-4">
373 + <h3 class="text-xs font-semibold uppercase tracking-wider mb-3 px-2" style="color: var(--color-light-focus-secondary);">
374 + Sección
375 + </h3>
376 + <div class="space-y-1">
377 + <!-- Items de navegación aquí -->
378 + </div>
379 + </div>
380 +
381 + </nav>
382 +
383 + <!-- Footer -->
384 + <div class="p-4 border-t flex-shrink-0" style="border-color: rgba(0, 0, 0, 0.05);">
385 + <p class="text-xs text-center" style="color: var(--color-light-focus-secondary);">
386 + © 2026
387 + </p>
388 + </div>
389 +
390 + </div>
391 +</aside>
392 +```
393 +
394 +---
395 +
396 +## 6. Iconos SVG
397 +
398 +### Sol (modo claro activo)
399 +```html
400 +<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
401 + <path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd"/>
402 +</svg>
403 +```
404 +
405 +### Luna (modo oscuro activo)
406 +```html
407 +<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
408 + <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
409 +</svg>
410 +```
411 +
412 +### Hamburguesa (menú)
413 +```html
414 +<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
415 + <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
416 +</svg>
417 +```
418 +
419 +### X (cerrar)
420 +```html
421 +<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
422 + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
423 +</svg>
424 +```
425 +
426 +### Home (4 cuadrados)
427 +```html
428 +<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
429 + <rect x="2" y="2" width="7" height="7" rx="1.5"/>
430 + <rect x="11" y="2" width="7" height="7" rx="1.5"/>
431 + <rect x="2" y="11" width="7" height="7" rx="1.5"/>
432 + <rect x="11" y="11" width="7" height="7" rx="1.5"/>
433 +</svg>
434 +```
435 +
436 +---
437 +
438 +## 7. Resumen de comportamiento
439 +
440 +| Viewport | Qué se ve | Theme toggle |
441 +|----------|-----------|--------------|
442 +| **Móvil** (`<768px`) | Solo hamburguesa arriba derecha | Dentro del drawer (arriba izquierda) |
443 +| **Desktop** (`≥768px`) | Home + Theme + Hamburguesa | En la barra de controles |
444 +
445 +### Animaciones
446 +
447 +| Elemento | Animación |
448 +|----------|-----------|
449 +| Hover en iconos | Color cambia de `texto-auxiliar``titulo` |
450 +| Tooltip | `opacity 0→1`, `invisible→visible` con `duration-200` |
451 +| Drawer abre | `translate3d(100%, 0, 0)``translate3d(0, 0, 0)` con `cubic-bezier(0.33, 1, 0.68, 1)` |
452 +| Overlay | `opacity 0→1` con `0.2s ease-out` |
1 +gestion,objeto,devengado
2 +2025,11100,7010195151.280003
3 +2025,11210,5812831318.110004
4 +2025,11220,3960643633.5099993
5 +2025,11310,477377380.7399999
6 +2025,11321,611358929.5699998
7 +2025,11322,344284069.2800001
8 +2025,11323,8492765.11
9 +2025,11324,119472850.73000005
10 +2025,11331,3741918.29
11 +2025,11332,222831.86
12 +2025,11339,991911009.5900005
13 +2025,11400,2598265843.6899953
14 +2025,11510,115618445.64
15 +2025,11520,3684768.92
16 +2025,11600,347937539.3
17 +2025,11700,16012422722.88999
18 +2025,11810,1897633.19
19 +2025,11820,5236920.13
20 +2025,11910,418427037.15000004
21 +2025,11920,49552831.83999999
22 +2025,11930,141140855.32999998
23 +2025,11940,4603197.24
24 +2025,12100,3204535824.6000023
25 +2025,13110,3901953807.7400007
26 +2025,13120,619414677.6200007
27 +2025,13131,1362025647.0700052
28 +2025,13132,16517337.85
29 +2025,13200,780321050.7899984
30 +2025,14100,1139341529.1000001
31 +2025,15100,0
32 +2025,15200,0
33 +2025,15300,0
34 +2025,15400,4691146.74
35 +2025,21100,18994647.069999997
36 +2025,21200,1185053965.7300012
37 +2025,21300,187133399.53000006
38 +2025,21400,53465476.169999935
39 +2025,21500,32025660.46999999
40 +2025,21600,194624988.81999987
41 +2025,22110,91267157.03
42 +2025,22120,18054096.22
43 +2025,22210,150388054.74000007
44 +2025,22220,34629325.89
45 +2025,22300,1959574229.3100035
46 +2025,22400,2341473.77
47 +2025,22500,701307983.6299998
48 +2025,22600,129774059.36000001
49 +2025,23100,177818037.08
50 +2025,23200,705817678.630001
51 +2025,23300,267332.29
52 +2025,23400,102877216.77999996
53 +2025,24110,347305124.4300007
54 +2025,24120,495875030.0700008
55 +2025,24130,4734068.450000001
56 +2025,24200,186441122.7200001
57 +2025,24300,2640074895.480003
58 +2025,25120,968577449.3700005
59 +2025,25130,48331129.9
60 +2025,25210,124279655.01999998
61 +2025,25220,1788157637.100003
62 +2025,25230,38750297.28
63 +2025,25300,561398734.3999997
64 +2025,25400,878953716.1200004
65 +2025,25500,306587141.63999945
66 +2025,25600,313083444.33000004
67 +2025,25700,6976563.890000001
68 +2025,25810,119466843.19999999
69 +2025,25820,224542509.50000012
70 +2025,25900,237665083.13999984
71 +2025,26200,15655261.7
72 +2025,26300,34604066.51
73 +2025,26610,123238100.68000004
74 +2025,26620,65803840.900000006
75 +2025,26630,5856917.720000001
76 +2025,26640,12578208.32
77 +2025,26700,14848951.62
78 +2025,26910,663385.5700000003
79 +2025,26920,235250
80 +2025,26930,8978735.689999998
81 +2025,26940,66563836.099999994
82 +2025,26990,1503662082.0000036
83 +2025,27110,1825790733.1499996
84 +2025,27120,364659427.3
85 +2025,31110,542443427.4200002
86 +2025,31120,216381918.43999982
87 +2025,31130,664933780.7900007
88 +2025,31140,546467610.53
89 +2025,31150,58934973.47
90 +2025,31200,33094567.35000001
91 +2025,31300,2447995585.1100035
92 +2025,32100,143276681.52000007
93 +2025,32200,132315063.70000002
94 +2025,32300,5561928.509999998
95 +2025,32400,159320
96 +2025,32500,1136682.8700000003
97 +2025,33100,24196989.06
98 +2025,33200,44402437.73000002
99 +2025,33300,140159482.78000006
100 +2025,33400,44503112.73000003
101 +2025,34110,1233206780.699983
102 +2025,34120,22281523272.74
103 +2025,34130,210854679.98
104 +2025,34200,2971103666.2100005
105 +2025,34300,107990911.47999994
106 +2025,34400,7452099.93
107 +2025,34500,643334205.2399997
108 +2025,34600,256516535.3800006
109 +2025,34700,4499642859.289998
110 +2025,34800,31720752.09
111 +2025,34900,3886088.93
112 +2025,39100,110497000.54
113 +2025,39200,48738034.710000016
114 +2025,39300,4137761.2899999996
115 +2025,39400,88709616.89000002
116 +2025,39500,308909532.08000034
117 +2025,39600,13789089.190000001
118 +2025,39700,260527546.88999993
119 +2025,39800,711478491.3899997
120 +2025,39911,25451599.84
121 +2025,39912,229716435.09
122 +2025,39990,85810785.37999989
123 +2025,41100,4018555.16
124 +2025,41200,11738231.740000002
125 +2025,41300,0
126 +2025,42210,2475688.8800000004
127 +2025,42220,5772449.3
128 +2025,42230,3644887583.5699887
129 +2025,42240,119884214.51000004
130 +2025,42310,3262092590.7700067
131 +2025,42320,162411814.5
132 +2025,42400,748574159.99
133 +2025,42500,368399418.97999996
134 +2025,43110,68317124.34
135 +2025,43120,223834030.73999995
136 +2025,43200,145257372.71
137 +2025,43310,18770602.6
138 +2025,43320,7603011.960000001
139 +2025,43330,290260838.97
140 +2025,43340,4231983.279999999
141 +2025,43400,398948895.01000005
142 +2025,43500,328470832.01999986
143 +2025,43600,51696765.46999999
144 +2025,43700,169086594.95000005
145 +2025,46110,125733899.86000003
146 +2025,46120,74691013.75999999
147 +2025,46210,57348534.56999999
148 +2025,46220,9583654.360000001
149 +2025,46310,30831570.299999986
150 +2025,46320,2784494.7899999996
151 +2025,49100,41480273
152 +2025,49300,492862.94999999995
153 +2025,49400,85275
154 +2025,49900,660922.38
155 +2025,51100,0
156 +2025,51200,620015014.37
157 +2025,51600,481042090
158 +2025,51700,29014116.87
159 +2025,53410,52050949.82
160 +2025,53420,351090144.51
161 +2025,53430,3378151.15
162 +2025,53440,25716472.12
163 +2025,54300,77289717.23
164 +2025,54800,632675260.65
165 +2025,55130,0
166 +2025,56100,2147721911.8500004
167 +2025,57100,836275.9
168 +2025,57200,0
169 +2025,61100,70227731033.1
170 +2025,61200,3503973693.6899977
171 +2025,61300,7686201.74
172 +2025,61400,165316.78
173 +2025,61600,4660620766.32
174 +2025,61700,3610015023.1800003
175 +2025,61800,578767.5200000001
176 +2025,61900,560031.13
177 +2025,62100,106585021.14999999
178 +2025,62200,38756619.58
179 +2025,62300,3500499.439999999
180 +2025,62400,405.55
181 +2025,62600,4871024753.510001
182 +2025,62700,3091378138.92
183 +2025,62800,66184743.53000001
184 +2025,62900,236631.16
185 +2025,63100,6480763.82
186 +2025,63200,0
187 +2025,63300,100360.43
188 +2025,63400,462857.7
189 +2025,63500,35529.2
190 +2025,63600,0
191 +2025,63700,2369055.65
192 +2025,63800,0
193 +2025,63900,1087614.1800000002
194 +2025,64100,0
195 +2025,64200,0
196 +2025,65100,586447491.8299999
197 +2025,65210,398987753.5799999
198 +2025,65220,240387686.01999998
199 +2025,65230,132581381.01
200 +2025,65240,0
201 +2025,65300,2444818356.6
202 +2025,65400,644864385.6300001
203 +2025,65500,797927308.79
204 +2025,65600,622100032.9
205 +2025,65800,6897016.98
206 +2025,65900,66705420.480000004
207 +2025,66100,403828926.02000004
208 +2025,66210,2597435653.4000015
209 +2025,66220,1829365908.85
210 +2025,66230,646299428.4800001
211 +2025,66240,80232240.73
212 +2025,66250,209697820.00000003
213 +2025,66300,239921321.82000005
214 +2025,66400,134798065.3299999
215 +2025,66900,1844595842.08
216 +2025,67100,23208064.040000003
217 +2025,68200,230881944.88000008
218 +2025,69100,29581681.77
219 +2025,69200,203500678.33
220 +2025,71100,7630007944.97
221 +2025,71210,2392898.6
222 +2025,71220,322149243.09
223 +2025,71230,4408152.6
224 +2025,71300,26644234.430000003
225 +2025,71610,1883091507.0600004
226 +2025,71630,343800974.93
227 +2025,71700,65080225.449999996
228 +2025,71800,4478984170.690001
229 +2025,72200,2046897058.8299997
230 +2025,72420,316010589.22
231 +2025,72520,7584584602.65001
232 +2025,73100,2664574022.970007
233 +2025,73200,24681254941.659996
234 +2025,73410,4845222515.11
235 +2025,73420,315258596.03999996
236 +2025,73430,4314037.58
237 +2025,73440,1652127
238 +2025,73700,15015040108.359999
239 +2025,73820,1364681.67
240 +2025,75110,0
241 +2025,75120,683772757.9200002
242 +2025,75211,469127620.05
243 +2025,75212,50308009.760000005
244 +2025,75221,11649919.909999998
245 +2025,75222,305831148.79999995
246 +2025,75320,0
247 +2025,77100,346626643.30999994
248 +2025,77200,1783047515.49
249 +2025,77410,154404059.40000007
250 +2025,77440,13005182.149999999
251 +2025,77520,34542653.12
252 +2025,77530,1359464924.2300012
253 +2025,77700,0
254 +2025,77820,42931153.11
255 +2025,78100,346333.4
256 +2025,79100,20183063.04
257 +2025,79200,74336.66
258 +2025,79310,69600
259 +2025,81100,189467352
260 +2025,81200,1327189189.5399992
261 +2025,81300,551652107.6699998
262 +2025,81400,299412884.65
263 +2025,81500,2442434
264 +2025,81600,395569
265 +2025,81950,39137
266 +2025,81960,32790665.14
267 +2025,81990,2007174694.16
268 +2025,82100,879007438.5400001
269 +2025,83110,29113391.8
270 +2025,83120,4240482.35
271 +2025,83210,351070.72
272 +2025,83220,50460
273 +2025,84100,65887753.279999994
274 +2025,84230,394819507.59
275 +2025,84240,723835763.5600002
276 +2025,84250,65803251.14
277 +2025,84900,8503359.91
278 +2025,85100,94254502.63999999
279 +2025,85200,10478575.300000003
280 +2025,85400,6221225.630000003
281 +2025,85500,3581209.3799999994
282 +2025,85900,20640115.529999997
283 +2025,86100,29028844.980000008
284 +2025,91200,512102826.06
285 +2025,92100,0
286 +2025,94100,11526054.86
287 +2025,94200,7279202.82
288 +2025,94300,1551638.3499999999
289 +2025,95100,291453722.56
290 +2025,96100,5934986991.019999
291 +2025,96200,614954091.44
292 +2025,96900,2947311655.2299995
293 +2025,97100,70099589.35000001
294 +2025,98300,125000000
295 +2025,99100,0
296 +2025,99200,0
This diff could not be displayed because it is too large.