Showing
16 changed files
with
4276 additions
and
204 deletions
| ... | @@ -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"> |
| 89 | + <div class="flex gap-2"> | ||
| 80 | <input | 90 | <input |
| 81 | bind:this={inputRef} | 91 | bind:this={inputRef} |
| 82 | type="text" | 92 | type="text" |
| 83 | - placeholder={$indexLoaded ? "Buscar entidad, objeto de gasto o rubro..." : "Cargando..."} | 93 | + placeholder={$indexLoaded ? "Buscar..." : "Cargando..."} |
| 84 | disabled={!$indexLoaded} | 94 | disabled={!$indexLoaded} |
| 85 | - class="w-full p-2 border rounded" | 95 | + class="flex-1 p-2 border rounded" |
| 86 | oninput={handleInput} | 96 | oninput={handleInput} |
| 87 | onkeydown={handleKeydown} | 97 | onkeydown={handleKeydown} |
| 88 | onfocus={() => isFocused = true} | 98 | onfocus={() => isFocused = true} |
| 89 | /> | 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} |
| 45 | + {#if showGroupHeaders} | ||
| 42 | <div class="px-3 py-1.5 bg-slate-50 border-b"> | 46 | <div class="px-3 py-1.5 bg-slate-50 border-b"> |
| 43 | <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Entidades</span> | 47 | <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Entidades</span> |
| 44 | </div> | 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} |
| 69 | + {#if showGroupHeaders} | ||
| 64 | <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 ? 'border-t' : ''}"> | 70 | <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 ? 'border-t' : ''}"> |
| 65 | <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Objetos del Gasto</span> | 71 | <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Objetos del Gasto</span> |
| 66 | </div> | 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} |
| 91 | + {#if showGroupHeaders} | ||
| 84 | <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 || groupedResults().objetos.length > 0 ? 'border-t' : ''}"> | 92 | <div class="px-3 py-1.5 bg-slate-50 border-b {groupedResults().entidades.length > 0 || groupedResults().objetos.length > 0 ? 'border-t' : ''}"> |
| 85 | <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Rubros</span> | 93 | <span class="text-xs font-medium text-slate-500 uppercase tracking-wide">Rubros</span> |
| 86 | </div> | 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> | ||
| 11 | </div> | 71 | </div> |
| 12 | </div> | 72 | </div> |
| 13 | -</nav> | 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> | ||
| 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 | + | ||
| 124 | + </div> | ||
| 125 | +</div> | ... | ... |
src/lib/stores/drawer.js
0 → 100644
| 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 --> |
| 193 | + <div class="grain"></div> | ||
| 194 | + | ||
| 195 | + <!-- Header con Logo y Controles --> | ||
| 196 | + <header class="page-header"> | ||
| 197 | + {#if isDarkMode} | ||
| 198 | + <img | ||
| 199 | + src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" | ||
| 200 | + alt="Ministerio de Economia y Finanzas Publicas" | ||
| 201 | + class="mefp-logo" | ||
| 202 | + /> | ||
| 203 | + {:else} | ||
| 204 | + <img | ||
| 205 | + src="/logos_claro/06_Isologo MEFP horizontal escala de grises.png" | ||
| 206 | + alt="Ministerio de Economia y Finanzas Publicas" | ||
| 207 | + class="mefp-logo" | ||
| 208 | + /> | ||
| 209 | + {/if} | ||
| 11 | 210 | ||
| 12 | - <div class="mb-8"> | 211 | + <!-- Controles: Theme Toggle + Menú --> |
| 13 | - <SearchBox /> | 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> | ||
| 14 | </div> | 238 | </div> |
| 15 | 239 | ||
| 16 | - <div class="grid grid-cols-2 gap-4 text-sm"> | 240 | + <!-- Botón Menú --> |
| 17 | - <a href="/clasificadores" class="p-4 border rounded hover:bg-gray-50"> | 241 | + <div class="relative group/menu"> |
| 18 | - <strong>Clasificadores</strong> | 242 | + <button |
| 19 | - <p class="text-gray-600">Navegar por instituciones, objetos de gasto, etc.</p> | 243 | + onclick={openDrawer} |
| 20 | - </a> | 244 | + class="p-2 rounded-full transition-all" |
| 21 | - <a href="/historias" class="p-4 border rounded hover:bg-gray-50"> | 245 | + style="background-color: transparent; color: var(--theme-texto); cursor: pointer;" |
| 22 | - <strong>Historias</strong> | 246 | + onmouseenter={(e) => e.currentTarget.style.color = 'var(--theme-titulo)'} |
| 23 | - <p class="text-gray-600">Análisis y visualizaciones del gasto público</p> | 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> | ||
| 24 | </a> | 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> | ||
| 25 | </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"> |
| 79 | + <!-- Breadcrumb --> | ||
| 80 | + <nav class="breadcrumb"> | ||
| 81 | + <a href="/" class="breadcrumb-link">← Inicio</a> | ||
| 82 | + </nav> | ||
| 83 | + <div class="header-content"> | ||
| 84 | + <h1>Formas de leer el presupuesto</h1> | ||
| 85 | + <p class="subtitulo"> | ||
| 86 | + Cada ingreso y cada gasto del Estado se registra bajo distintas clasificaciones. | ||
| 87 | + Aquí puedes explorar los datos según cada una de ellas. | ||
| 22 | </p> | 88 | </p> |
| 89 | + </div> | ||
| 90 | + </header> | ||
| 23 | 91 | ||
| 24 | - <h1 class="text-2xl mb-6">Clasificadores</h1> | 92 | + <!-- Grid de clasificadores --> |
| 25 | - | 93 | + <section class="grid-clasificadores"> |
| 26 | - <div class="space-y-2"> | 94 | + {#each clasificadores as c} |
| 27 | - {#each clasificadores as item} | 95 | + <a href={c.ruta} class="card"> |
| 28 | - {#if item.disponible} | 96 | + <span class="card-meta"> |
| 29 | - <a href="/clasificadores/{item.id}" class="block p-4 border rounded hover:bg-gray-50"> | 97 | + <span class="card-numero">{c.numero}</span> |
| 30 | - <strong>{item.nombre}</strong> | 98 | + <span class="card-nombre">{c.nombre}</span> |
| 31 | - <p class="text-sm text-gray-600">{item.descripcion}</p> | 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> | ||
| 32 | </a> | 103 | </a> |
| 33 | - {:else} | ||
| 34 | - <div class="p-4 border rounded opacity-50"> | ||
| 35 | - <strong>{item.nombre}</strong> | ||
| 36 | - <span class="text-xs text-gray-400 ml-2">(próximamente)</span> | ||
| 37 | - <p class="text-sm text-gray-600">{item.descripcion}</p> | ||
| 38 | - </div> | ||
| 39 | - {/if} | ||
| 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 | > |
| 441 | + <div class="flex items-start justify-between gap-3"> | ||
| 277 | <div class="flex-1 min-w-0"> | 442 | <div class="flex-1 min-w-0"> |
| 278 | - <div class="flex items-center gap-2"> | 443 | + <div class="flex items-center gap-2 flex-wrap"> |
| 279 | - <span class="text-slate-900">{entidad.nombre}</span> | 444 | + <span class="font-mono text-xs" style="color: var(--theme-texto);">{entidad.codigo}</span> |
| 445 | + <span class="font-medium" style="color: var(--theme-titulo);">{entidad.nombre}</span> | ||
| 280 | {#if entidad.sigla} | 446 | {#if entidad.sigla} |
| 281 | - <span class="text-sm text-slate-400">({entidad.sigla})</span> | 447 | + <span class="text-sm" style="color: var(--theme-texto); opacity: 0.7;">({entidad.sigla})</span> |
| 282 | {/if} | 448 | {/if} |
| 283 | </div> | 449 | </div> |
| 284 | - <p class="text-xs text-slate-400 mt-0.5"> | 450 | + <p class="text-xs mt-1" style="font-family: 'DM Mono', monospace; color: var(--theme-texto);"> |
| 285 | - Código: {entidad.codigo} · {formatGestiones(entidad.gestiones)} | 451 | + {formatGestiones(entidad.gestiones)} |
| 286 | </p> | 452 | </p> |
| 287 | </div> | 453 | </div> |
| 288 | - <svg class="w-4 h-4 text-slate-300 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | 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"> |
| 289 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /> | 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" /> |
| 290 | </svg> | 456 | </svg> |
| 457 | + </div> | ||
| 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> | ||
| 852 | + </nav> | ||
| 853 | + | ||
| 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> | ||
| 192 | </nav> | 932 | </nav> |
| 193 | - <div class="flex items-center justify-between"> | 933 | + </div> |
| 194 | - <h1 class="text-2xl font-light text-slate-900">Clasificador por Objeto del Gasto</h1> | 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 | × | 1928 | × |
| 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> | ... | ... |
static/ESTILOS_UI.md
0 → 100644
| 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` | |
static/data.csv
0 → 100644
| 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 |
static/treemap_objeto_estado.csv
0 → 100644
This diff could not be displayed because it is too large.
-
Please register or login to post a comment