Showing
15 changed files
with
5662 additions
and
1248 deletions
docs/json
0 → 100644
| 1 | +{ | ||
| 2 | + "facet_counts": [], | ||
| 3 | + "found": 49, | ||
| 4 | + "out_of": 735538, | ||
| 5 | + "page": 1, | ||
| 6 | + "search_time_ms": 96, | ||
| 7 | + "hits": [ | ||
| 8 | + { | ||
| 9 | + "document": { | ||
| 10 | + "gestion": 2017, | ||
| 11 | + "devengado": 6544172.35, | ||
| 12 | + "is_class": false, | ||
| 13 | + "class_": "programa_proyecto", | ||
| 14 | + "texto": "CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA LINERA-COPALANI", | ||
| 15 | + "metadatos": { | ||
| 16 | + "entidad": 1260, | ||
| 17 | + "programa": 21, | ||
| 18 | + "proyecto": "112", | ||
| 19 | + "actividad": 0.0 | ||
| 20 | + } | ||
| 21 | + }, | ||
| 22 | + "highlights": [ | ||
| 23 | + { | ||
| 24 | + "field": "texto", | ||
| 25 | + "matched_tokens": ["LINERA"], | ||
| 26 | + "snippet": "CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA <mark>LINERA</mark>-COPALANI" | ||
| 27 | + } | ||
| 28 | + ] | ||
| 29 | + }, | ||
| 30 | + { | ||
| 31 | + "document": { | ||
| 32 | + "gestion": 2018, | ||
| 33 | + "devengado": 6500010.17, | ||
| 34 | + "is_class": false, | ||
| 35 | + "class_": "programa_proyecto", | ||
| 36 | + "texto": "CONST. INSTITUTO TECNOLOGICO ALVARO GARCIA LINERA", | ||
| 37 | + "metadatos": { | ||
| 38 | + "entidad": 1347, | ||
| 39 | + "programa": 21, | ||
| 40 | + "proyecto": "110", | ||
| 41 | + "actividad": 0.0 | ||
| 42 | + } | ||
| 43 | + }, | ||
| 44 | + "highlights": [ | ||
| 45 | + { | ||
| 46 | + "field": "texto", | ||
| 47 | + "matched_tokens": ["LINERA"], | ||
| 48 | + "snippet": "CONST. INSTITUTO TECNOLOGICO ALVARO GARCIA <mark>LINERA</mark>" | ||
| 49 | + } | ||
| 50 | + ] | ||
| 51 | + } | ||
| 52 | + ] | ||
| 53 | +} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
docs/refactor-buscador.md
0 → 100644
| 1 | +# Refactor del Buscador - Landing Page | ||
| 2 | + | ||
| 3 | +## Naturaleza del Proyecto | ||
| 4 | + | ||
| 5 | +**¿Para quién?** Ciudadanos bolivianos (público general, no técnicos) | ||
| 6 | + | ||
| 7 | +**¿De quién?** Gobierno boliviano - Ministerio de Economía y Finanzas Públicas | ||
| 8 | + | ||
| 9 | +**¿Qué es?** Datos abiertos de contabilidad fiscal (presupuesto público) | ||
| 10 | + | ||
| 11 | +**El mensaje central**: "Puedes buscar donde quieras" - transparencia, accesibilidad, apertura | ||
| 12 | + | ||
| 13 | +El buscador de la landing actual es un **statement visual** de ese mensaje. El nuevo buscador debe mantener ese espíritu. | ||
| 14 | + | ||
| 15 | +--- | ||
| 16 | + | ||
| 17 | +## Nueva Ruta: `/nuevo-buscador` | ||
| 18 | + | ||
| 19 | +Ruta dedicada al buscador completo con todas las capacidades: | ||
| 20 | +- Selector Ingresos/Gastos | ||
| 21 | +- Selector Programas/Clasificadores (solo gastos) | ||
| 22 | +- Selector de clasificador específico | ||
| 23 | +- Resultados ricos tipo Spotlight | ||
| 24 | + | ||
| 25 | +**Archivos**: | ||
| 26 | +``` | ||
| 27 | +src/routes/nuevo-buscador/ | ||
| 28 | + └── +page.svelte | ||
| 29 | +``` | ||
| 30 | + | ||
| 31 | +--- | ||
| 32 | + | ||
| 33 | +## Command K (Buscador Global) - EFECTO DEL REFACTOR | ||
| 34 | + | ||
| 35 | +### Descripción | ||
| 36 | +Hay un buscador modal global (tipo Spotlight) que se activa con `Cmd+K` / `Ctrl+K` desde cualquier página. Este componente **debe sincronizarse** con las decisiones del buscador principal. | ||
| 37 | + | ||
| 38 | +### Archivos involucrados | ||
| 39 | +``` | ||
| 40 | +src/lib/components/ui/SearchModal.svelte # Componente del modal | ||
| 41 | +src/routes/+layout.svelte # Monta el modal globalmente (línea 67) | ||
| 42 | +src/lib/components/ui/Navbar.svelte # Botón para abrir el modal | ||
| 43 | +src/lib/stores/searchStore.js # Store compartido con landing | ||
| 44 | +``` | ||
| 45 | + | ||
| 46 | +### Implementación actual | ||
| 47 | +- **Activación**: `Cmd+K` / `Ctrl+K` (definido en `+layout.svelte:23-28`) | ||
| 48 | +- **También**: Botón en el Navbar (`onOpenSearch`) | ||
| 49 | +- **Filtros actuales**: 6 clasificadores (entidad, objeto_gasto, rubro, finfun, organismo, fuente) | ||
| 50 | +- **Store**: Usa `searchStore.js` (compartido con landing) | ||
| 51 | +- **NO tiene**: Selector Ingresos/Gastos, Programas/Proyectos | ||
| 52 | + | ||
| 53 | +### Lo que debe cambiar con el refactor | ||
| 54 | +1. Agregar selector Ingresos/Gastos | ||
| 55 | +2. Agregar opción Programas/Proyectos (solo en Gastos) | ||
| 56 | +3. Sincronizar filtros con el buscador principal | ||
| 57 | +4. Posiblemente: recordar última selección del usuario | ||
| 58 | +5. Mostrar las 3 variaciones de diseño de resultados | ||
| 59 | + | ||
| 60 | +### Flujo deseado post-refactor | ||
| 61 | +``` | ||
| 62 | +Usuario en cualquier página | ||
| 63 | + │ | ||
| 64 | + ▼ | ||
| 65 | + [Cmd+K] | ||
| 66 | + │ | ||
| 67 | + ▼ | ||
| 68 | +┌─────────────────────────────────────┐ | ||
| 69 | +│ SearchModal (replica del central) │ | ||
| 70 | +│ │ | ||
| 71 | +│ [Ingresos] [Gastos] │ | ||
| 72 | +│ [Programas] [Clasificadores] │ | ||
| 73 | +│ │ | ||
| 74 | +│ Resultados según selección... │ | ||
| 75 | +└─────────────────────────────────────┘ | ||
| 76 | +``` | ||
| 77 | + | ||
| 78 | +--- | ||
| 79 | + | ||
| 80 | +## Conceptos Clave | ||
| 81 | + | ||
| 82 | +### Selector Central: Ingresos vs Gastos | ||
| 83 | +Este es el **eje fundamental** que contextualiza toda búsqueda. No es un filtro opcional - define el universo de datos. | ||
| 84 | + | ||
| 85 | +### Clasificadores Compartidos (con contexto distinto) | ||
| 86 | +| Clasificador | En INGRESOS significa | En GASTOS significa | | ||
| 87 | +|--------------|----------------------|---------------------| | ||
| 88 | +| **Geográfico** (dónde) | ¿De dónde vienen los ingresos? | ¿Dónde se ejecuta el gasto? | | ||
| 89 | +| **Entidades** (quién) | ¿Quién recibe? | ¿Quién ejecuta? | | ||
| 90 | + | ||
| 91 | +--- | ||
| 92 | + | ||
| 93 | +## Árbol de Decisiones de Búsqueda | ||
| 94 | + | ||
| 95 | +``` | ||
| 96 | + ┌─────────────────┐ | ||
| 97 | + │ DEFAULT │ | ||
| 98 | + │ Gastos+Programas│ | ||
| 99 | + └────────┬────────┘ | ||
| 100 | + │ | ||
| 101 | + ┌───────────────┴───────────────┐ | ||
| 102 | + ▼ ▼ | ||
| 103 | + ┌───────────────┐ ┌───────────────┐ | ||
| 104 | + │ INGRESOS │ │ GASTOS │ | ||
| 105 | + └───────┬───────┘ └───────┬───────┘ | ||
| 106 | + │ │ | ||
| 107 | + │ ┌───────┴───────┐ | ||
| 108 | + │ ▼ ▼ | ||
| 109 | + │ ┌─────────────┐ ┌─────────────┐ | ||
| 110 | + │ │ PROGRAMAS/ │ │ CLASIFIC. │ | ||
| 111 | + │ │ PROYECTOS │ │ │ | ||
| 112 | + │ │ (~600k) │ │ │ | ||
| 113 | + │ └─────────────┘ └──────┬──────┘ | ||
| 114 | + │ │ | ||
| 115 | + ▼ ▼ | ||
| 116 | + ┌───────────────┐ ┌───────────────┐ | ||
| 117 | + │ CLASIFICADORES│ │ CLASIFICADORES│ | ||
| 118 | + │ INGRESOS │ │ GASTOS │ | ||
| 119 | + └───────┬───────┘ └───────┬───────┘ | ||
| 120 | + │ │ | ||
| 121 | + ┌────────────┼────────────┐ ┌─────────────┼─────────────┐ | ||
| 122 | + ▼ ▼ ▼ ▼ ▼ ▼ | ||
| 123 | +┌────────┐ ┌──────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ | ||
| 124 | +│ Rubro │ │Organismo │ │ Fuente │ │ Fin/Fun │ │Obj.Gasto │ │ Sectores │ | ||
| 125 | +└────────┘ └──────────┘ └────────┘ └──────────┘ └──────────┘ └──────────┘ | ||
| 126 | + │ │ | ||
| 127 | + ▼ ▼ | ||
| 128 | + ┌───────────────┐ ┌───────────────┐ | ||
| 129 | + │ Geográfico │ (compartido) │ Geográfico │ | ||
| 130 | + │ (dónde) │ │ (dónde) │ | ||
| 131 | + └───────────────┘ └───────────────┘ | ||
| 132 | + │ │ | ||
| 133 | + ▼ ▼ | ||
| 134 | + ┌───────────────┐ ┌───────────────┐ | ||
| 135 | + │ Entidades │ (compartido) │ Entidades │ | ||
| 136 | + │ (quién) │ │ (quién) │ | ||
| 137 | + └───────────────┘ └───────────────┘ | ||
| 138 | +``` | ||
| 139 | + | ||
| 140 | +--- | ||
| 141 | + | ||
| 142 | +## Clasificadores por Modo | ||
| 143 | + | ||
| 144 | +| INGRESOS (5) | GASTOS (5) | | ||
| 145 | +|--------------|------------| | ||
| 146 | +| Rubro | Finalidad y Función | | ||
| 147 | +| Organismo | Objetos de Gasto | | ||
| 148 | +| Fuente | Sectores | | ||
| 149 | +| Geográfico *(dónde)* | Geográfico *(dónde)* | | ||
| 150 | +| Entidades *(quién)* | Entidades *(quién)* | | ||
| 151 | + | ||
| 152 | +**Nota**: Geográfico y Entidades aparecen en ambos pero la vista enfatiza el contexto (ingresos o gastos) según la rama seleccionada. | ||
| 153 | + | ||
| 154 | +--- | ||
| 155 | + | ||
| 156 | +## Tabla de Todas las Búsquedas Posibles | ||
| 157 | + | ||
| 158 | +| # | Modo | Submodo | Clasificador | Qué busca | | ||
| 159 | +|---|------|---------|--------------|-----------| | ||
| 160 | +| **0** | **DEFAULT** | - | - | **Gastos + Programas/Proyectos** | | ||
| 161 | +| **INGRESOS** ||||| | ||
| 162 | +| 1 | Ingresos | Clasif. | Todos | 5 clasificadores de ingresos | | ||
| 163 | +| 2 | Ingresos | Clasif. | Rubro | Solo rubros | | ||
| 164 | +| 3 | Ingresos | Clasif. | Organismo | Solo organismos | | ||
| 165 | +| 4 | Ingresos | Clasif. | Fuente | Solo fuentes | | ||
| 166 | +| 5 | Ingresos | Clasif. | Geográfico | Geográfico (contexto ingresos) | | ||
| 167 | +| 6 | Ingresos | Clasif. | Entidades | Entidades (contexto ingresos) | | ||
| 168 | +| **GASTOS** ||||| | ||
| 169 | +| 7 | Gastos | **Prog/Proy** | - | ~600k registros narrativos | | ||
| 170 | +| 8 | Gastos | Clasif. | Todos | 5 clasificadores de gastos | | ||
| 171 | +| 9 | Gastos | Clasif. | Finalidad/Función | Solo finfun | | ||
| 172 | +| 10 | Gastos | Clasif. | Objeto de gasto | Solo objetos | | ||
| 173 | +| 11 | Gastos | Clasif. | Sectores | Solo sectores | | ||
| 174 | +| 12 | Gastos | Clasif. | Geográfico | Geográfico (contexto gastos) | | ||
| 175 | +| 13 | Gastos | Clasif. | Entidades | Entidades (contexto gastos) | | ||
| 176 | + | ||
| 177 | +**Total: 14 formas de búsqueda** (1 default + 6 ingresos + 7 gastos) | ||
| 178 | + | ||
| 179 | +--- | ||
| 180 | + | ||
| 181 | +## Comportamiento Default | ||
| 182 | + | ||
| 183 | +``` | ||
| 184 | +┌─────────────────────────────────────────────────────────┐ | ||
| 185 | +│ [🔍 Buscar...] │ | ||
| 186 | +│ │ | ||
| 187 | +│ SIN SELECCIONAR NADA: │ | ||
| 188 | +│ → Busca en GASTOS + PROGRAMAS/PROYECTOS │ | ||
| 189 | +│ → Lo más intuitivo para el ciudadano común │ | ||
| 190 | +│ │ | ||
| 191 | +└─────────────────────────────────────────────────────────┘ | ||
| 192 | +``` | ||
| 193 | + | ||
| 194 | +**Razón**: Los programas/proyectos tienen nombres narrativos como "CONST. PISCINA OLIMPICA MUNICIPAL" que coinciden con cómo la gente piensa sobre el gasto público. | ||
| 195 | + | ||
| 196 | +--- | ||
| 197 | + | ||
| 198 | +## Las 3 Variaciones de Diseño | ||
| 199 | + | ||
| 200 | +Aunque hay 14 formas de búsqueda posibles, se reducen a **3 variaciones de diseño** de resultados: | ||
| 201 | + | ||
| 202 | +| Variación | Cuándo aplica | Características | | ||
| 203 | +|-----------|---------------|-----------------| | ||
| 204 | +| **1. Programas puro** | Default, Gastos + Programas | Cards narrativas ricas, contexto amplio | | ||
| 205 | +| **2. Clasificador individual** | Cualquier clasificador específico | Lista estructurada, jerárquica | | ||
| 206 | +| **3. Todos los clasificadores** | Ingresos/Gastos + "Todos" | Resultados agrupados por tipo | | ||
| 207 | + | ||
| 208 | +--- | ||
| 209 | + | ||
| 210 | +### Variación 1: PROGRAMAS PURO | ||
| 211 | + | ||
| 212 | +**Aplica cuando**: Default, o Gastos + Programas/Proyectos | ||
| 213 | + | ||
| 214 | +**Características**: | ||
| 215 | +- Texto narrativo libre (~600k registros) | ||
| 216 | +- Cards ricas con mucho contexto | ||
| 217 | +- Información: entidad, ubicación, monto, período, ejecución | ||
| 218 | + | ||
| 219 | +``` | ||
| 220 | +┌─────────────────────────────────────────────────────────┐ | ||
| 221 | +│ 🔍 piscina olimpica │ | ||
| 222 | +├─────────────────────────────────────────────────────────┤ | ||
| 223 | +│ │ | ||
| 224 | +│ ┌─────────────────────────────────────────────────┐ │ | ||
| 225 | +│ │ CONST. PISCINA OLIMPICA MUNICIPAL EVO MORALES │ │ | ||
| 226 | +│ │ ▸ Cobija · GAM Cobija · Bs 2.3M · 2019-2023 │ │ | ||
| 227 | +│ │ ████████░░ 80% ejecutado │ │ | ||
| 228 | +│ └─────────────────────────────────────────────────┘ │ | ||
| 229 | +│ ┌─────────────────────────────────────────────────┐ │ | ||
| 230 | +│ │ PISCINA OLIMPICA DEPARTAMENTAL │ │ | ||
| 231 | +│ │ ▸ Tarija · Gobernación · Bs 890k · 2021 │ │ | ||
| 232 | +│ │ ███░░░░░░░ 30% ejecutado │ │ | ||
| 233 | +│ └─────────────────────────────────────────────────┘ │ | ||
| 234 | +│ │ | ||
| 235 | +└─────────────────────────────────────────────────────────┘ | ||
| 236 | +``` | ||
| 237 | + | ||
| 238 | +--- | ||
| 239 | + | ||
| 240 | +### Variación 2: CLASIFICADOR INDIVIDUAL | ||
| 241 | + | ||
| 242 | +**Aplica cuando**: Se selecciona UN clasificador específico (rubro, objeto, finfun, etc.) | ||
| 243 | + | ||
| 244 | +**Características**: | ||
| 245 | +- Lista estructurada y limpia | ||
| 246 | +- Muestra jerarquía/nivel | ||
| 247 | +- Código + descripción | ||
| 248 | + | ||
| 249 | +``` | ||
| 250 | +┌─────────────────────────────────────────────────────────┐ | ||
| 251 | +│ 🔍 construccion [Objetos de gasto ▾] │ | ||
| 252 | +├─────────────────────────────────────────────────────────┤ | ||
| 253 | +│ │ | ||
| 254 | +│ 42000 · Construcciones y mejoras de bienes │ | ||
| 255 | +│ ▸ Partida · Nivel 2 │ | ||
| 256 | +│ │ | ||
| 257 | +│ 42100 · Construcciones de bienes de dominio público │ | ||
| 258 | +│ ▸ Partida específica · Nivel 3 │ | ||
| 259 | +│ │ | ||
| 260 | +│ 42200 · Construcciones de bienes de dominio privado │ | ||
| 261 | +│ ▸ Partida específica · Nivel 3 │ | ||
| 262 | +│ │ | ||
| 263 | +└─────────────────────────────────────────────────────────┘ | ||
| 264 | +``` | ||
| 265 | + | ||
| 266 | +--- | ||
| 267 | + | ||
| 268 | +### Variación 3: TODOS LOS CLASIFICADORES | ||
| 269 | + | ||
| 270 | +**Aplica cuando**: Se selecciona "Todos" los clasificadores (de ingresos o de gastos) | ||
| 271 | + | ||
| 272 | +**Características**: | ||
| 273 | +- Resultados agrupados por tipo de clasificador | ||
| 274 | +- Secciones visuales separadas | ||
| 275 | +- Mezcla de estructuras | ||
| 276 | + | ||
| 277 | +``` | ||
| 278 | +┌─────────────────────────────────────────────────────────┐ | ||
| 279 | +│ 🔍 educacion [Todos gastos ▾] │ | ||
| 280 | +├─────────────────────────────────────────────────────────┤ | ||
| 281 | +│ │ | ||
| 282 | +│ FINALIDAD Y FUNCIÓN │ | ||
| 283 | +│ ┌─────────────────────────────────────────────────┐ │ | ||
| 284 | +│ │ 09 · Educación │ │ | ||
| 285 | +│ │ ▸ Finalidad │ │ | ||
| 286 | +│ └─────────────────────────────────────────────────┘ │ | ||
| 287 | +│ │ | ||
| 288 | +│ OBJETOS DE GASTO │ | ||
| 289 | +│ ┌─────────────────────────────────────────────────┐ │ | ||
| 290 | +│ │ 25800 · Servicios de capacitación │ │ | ||
| 291 | +│ │ ▸ Partida │ │ | ||
| 292 | +│ └─────────────────────────────────────────────────┘ │ | ||
| 293 | +│ │ | ||
| 294 | +│ ENTIDADES │ | ||
| 295 | +│ ┌─────────────────────────────────────────────────┐ │ | ||
| 296 | +│ │ Min. de Educación │ │ | ||
| 297 | +│ │ ▸ Ministerio · MINEDU │ │ | ||
| 298 | +│ └─────────────────────────────────────────────────┘ │ | ||
| 299 | +│ │ | ||
| 300 | +└─────────────────────────────────────────────────────────┘ | ||
| 301 | +``` | ||
| 302 | + | ||
| 303 | +--- | ||
| 304 | + | ||
| 305 | +## Arquitectura Técnica | ||
| 306 | + | ||
| 307 | +### Motor de Búsqueda | ||
| 308 | +**Typesense** para todo: | ||
| 309 | +- Clasificadores (~1,400 items) | ||
| 310 | +- Programas/Proyectos (~600,000 items) | ||
| 311 | + | ||
| 312 | +### Parámetros de Typesense (ya implementados) | ||
| 313 | +1. **Modo**: Ingresos vs Gastos | ||
| 314 | +2. **Submodo**: Programas vs Clasificadores (solo gastos) | ||
| 315 | +3. **Clasificador específico**: Cuál(es) clasificadores consultar | ||
| 316 | + | ||
| 317 | +### Archivos Principales | ||
| 318 | +``` | ||
| 319 | +src/routes/+page.svelte # Landing con buscador | ||
| 320 | +src/lib/services/search.js # Lógica de búsqueda Typesense | ||
| 321 | +src/lib/stores/searchStore.js # Estado reactivo | ||
| 322 | +src/lib/components/search/ | ||
| 323 | + ├── SearchBox.svelte # Input reutilizable | ||
| 324 | + ├── SearchResults.svelte # Dropdown resultados | ||
| 325 | + └── SearchResultItem.svelte # Item individual | ||
| 326 | +``` | ||
| 327 | + | ||
| 328 | +--- | ||
| 329 | + | ||
| 330 | +## Tareas de Implementación | ||
| 331 | + | ||
| 332 | +### Fase 1: UI del Selector | ||
| 333 | +- [ ] Diseñar selector Ingresos/Gastos (visible pero no obligatorio) | ||
| 334 | +- [ ] Diseñar selector Programas/Clasificadores (solo si Gastos) | ||
| 335 | +- [ ] Diseñar selector de clasificador específico | ||
| 336 | + | ||
| 337 | +### Fase 2: Conexión UI ↔ Typesense | ||
| 338 | +- [ ] Mapear estado de selectores a parámetros de Typesense | ||
| 339 | +- [ ] Implementar búsqueda default (Gastos + Programas) | ||
| 340 | +- [ ] Implementar cada una de las 14 formas de búsqueda | ||
| 341 | + | ||
| 342 | +### Fase 3: Diseño de Resultados | ||
| 343 | +- [ ] Diseñar cards ricas para Programas/Proyectos | ||
| 344 | +- [ ] Diseñar cards para cada tipo de clasificador | ||
| 345 | +- [ ] Implementar agrupación visual por tipo | ||
| 346 | + | ||
| 347 | +### Fase 4: Testing | ||
| 348 | +- [ ] Probar las 14 formas de búsqueda | ||
| 349 | +- [ ] Probar UX del selector | ||
| 350 | +- [ ] Probar performance con resultados mixtos | ||
| 351 | + | ||
| 352 | +--- | ||
| 353 | + | ||
| 354 | +*Documento creado: 2026-04-04* | ||
| 355 | +*Última actualización: 2026-04-04* |
docs/respuesta.json
0 → 100644
| 1 | +{"facet_counts":[],"found":49,"out_of":735538,"page":1,"search_time_ms":96,"hits":[{"document":{"gestion":2017,"devengado":6544172.35,"is_class":false,"class_":"programa_proyecto","texto":"CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA LINERA-COPALANI","metadatos":"{\"entidad\": 1260, \"programa\": 21, \"proyecto\": \"112\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA <mark>LINERA</mark>-COPALANI"}]},{"document":{"gestion":2018,"devengado":6500010.17,"is_class":false,"class_":"programa_proyecto","texto":"CONST. INSTITUTO TECNOLOGICO ALVARO GARCIA LINERA","metadatos":"{\"entidad\": 1347, \"programa\": 21, \"proyecto\": \"110\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. INSTITUTO TECNOLOGICO ALVARO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2019,"devengado":5553528.35,"is_class":false,"class_":"programa_proyecto","texto":"CONST. U.E. ALVARO GARCIA LINERA (MOLLE HUMA)","metadatos":"{\"entidad\": 1315, \"programa\": 21, \"proyecto\": \"013159003900000\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. U.E. ALVARO GARCIA <mark>LINERA</mark> (MOLLE HUMA)"}]},{"document":{"gestion":2018,"devengado":3614683.9000000004,"is_class":false,"class_":"programa_proyecto","texto":"CONST. AULAS, TINGLADO Y GRADERIAS U.E. ALVARO GARCIA LINERA","metadatos":"{\"entidad\": 1345, \"programa\": 21, \"proyecto\": \"4\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. AULAS, TINGLADO Y GRADERIAS U.E. ALVARO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2017,"devengado":3613875.74,"is_class":false,"class_":"programa_proyecto","texto":"CONST. AULAS, TINGLADO Y GRADERIAS U.E. ALVARO GARCIA LINERA","metadatos":"{\"entidad\": 1345, \"programa\": 21, \"proyecto\": \"22\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. AULAS, TINGLADO Y GRADERIAS U.E. ALVARO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2019,"devengado":3500005.9299999997,"is_class":false,"class_":"programa_proyecto","texto":"CONST. INSTITUTO TECNOLOGICO ALVARO GARCIA LINERA","metadatos":"{\"entidad\": 1347, \"programa\": 21, \"proyecto\": \"013470026900000\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. INSTITUTO TECNOLOGICO ALVARO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2018,"devengado":2975934.63,"is_class":false,"class_":"programa_proyecto","texto":"CONST. UNIDAD EDUC. MARCELO QUIROGA SANTA CRUZ BARRIO GARCIA LINERA","metadatos":"{\"entidad\": 1901, \"programa\": 21, \"proyecto\": \"49\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. UNIDAD EDUC. MARCELO QUIROGA SANTA CRUZ BARRIO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2017,"devengado":2673975.66,"is_class":false,"class_":"programa_proyecto","texto":"CONST. UNIDAD EDUCATIVA ALVARO GARCIA LINERA","metadatos":"{\"entidad\": 1305, \"programa\": 21, \"proyecto\": \"2\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. UNIDAD EDUCATIVA ALVARO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2018,"devengado":2080916.4999999998,"is_class":false,"class_":"programa_proyecto","texto":"CONST. DE 1 PUENTE TIPO CAJÓN SOBRE EL ARROYO GARCÍA LINERA ","metadatos":"{\"entidad\": 909, \"programa\": 14, \"proyecto\": \"5\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. DE 1 PUENTE TIPO CAJÓN SOBRE EL ARROYO GARCÍA <mark>LINERA</mark> "}]},{"document":{"gestion":2018,"devengado":1344102.25,"is_class":false,"class_":"programa_proyecto","texto":"CONST. U.E. ALVARO GARCIA LINERA (MOLLE HUMA)","metadatos":"{\"entidad\": 1315, \"programa\": 21, \"proyecto\": \"4\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. U.E. ALVARO GARCIA <mark>LINERA</mark> (MOLLE HUMA)"}]},{"document":{"gestion":2018,"devengado":1329560.05,"is_class":false,"class_":"programa_proyecto","texto":"CONST. EDIFICIO MULTIFUNCIONAL MUNICIPAL LIC. ALVARO GARCIA LINERA - OMEREQUE","metadatos":"{\"entidad\": 1309, \"programa\": 17, \"proyecto\": \"3\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. EDIFICIO MULTIFUNCIONAL MUNICIPAL LIC. ALVARO GARCIA <mark>LINERA</mark> - OMEREQUE"}]},{"document":{"gestion":2016,"devengado":1187131.78,"is_class":false,"class_":"programa_proyecto","texto":"CONST. 8 AULAS U.E. ALVARO GARCIA LINERA - ENTRE RIOS","metadatos":"{\"entidad\": 1345, \"programa\": 21, \"proyecto\": \"18\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. 8 AULAS U.E. ALVARO GARCIA <mark>LINERA</mark> - ENTRE RIOS"}]},{"document":{"gestion":2016,"devengado":777315.61,"is_class":false,"class_":"programa_proyecto","texto":"CONST. OBRA DE TOMA SISTEMA AGUA POTABLE BARRIO GARCIA LINERA ZONA TERMINAL","metadatos":"{\"entidad\": 1901, \"programa\": 11, \"proyecto\": \"66\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. OBRA DE TOMA SISTEMA AGUA POTABLE BARRIO GARCIA <mark>LINERA</mark> ZONA TERMINAL"}]},{"document":{"gestion":2018,"devengado":763230.3600000001,"is_class":false,"class_":"programa_proyecto","texto":"CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA LINERA-COPALANI","metadatos":"{\"entidad\": 1260, \"programa\": 21, \"proyecto\": \"112\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA <mark>LINERA</mark>-COPALANI"}]},{"document":{"gestion":2018,"devengado":697049.14,"is_class":false,"class_":"programa_proyecto","texto":"CONST. UNIDAD EDUCATIVA ALVARO GARCIA LINERA","metadatos":"{\"entidad\": 1305, \"programa\": 21, \"proyecto\": \"2\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. UNIDAD EDUCATIVA ALVARO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2019,"devengado":591398.0800000001,"is_class":false,"class_":"programa_proyecto","texto":"CONST. UNIDAD EDUC. MARCELO QUIROGA SANTA CRUZ BARRIO GARCIA LINERA","metadatos":"{\"entidad\": 1901, \"programa\": 21, \"proyecto\": \"019010034700000\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. UNIDAD EDUC. MARCELO QUIROGA SANTA CRUZ BARRIO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2017,"devengado":458997.1,"is_class":false,"class_":"programa_proyecto","texto":"CONST. UNIDAD EDUC. MARCELO QUIROGA SANTA CRUZ BARRIO GARCIA LINERA","metadatos":"{\"entidad\": 1901, \"programa\": 21, \"proyecto\": \"49\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. UNIDAD EDUC. MARCELO QUIROGA SANTA CRUZ BARRIO GARCIA <mark>LINERA</mark>"}]},{"document":{"gestion":2016,"devengado":412433.03,"is_class":false,"class_":"programa_proyecto","texto":"CONST. SIST. AGUA POTABLE BARRIO GARCIA LINERA (COBIJA)","metadatos":"{\"entidad\": 1901, \"programa\": 11, \"proyecto\": \"9001\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. SIST. AGUA POTABLE BARRIO GARCIA <mark>LINERA</mark> (COBIJA)"}]},{"document":{"gestion":2019,"devengado":382206.88,"is_class":false,"class_":"programa_proyecto","texto":"CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA LINERA-COPALANI","metadatos":"{\"entidad\": 1260, \"programa\": 21, \"proyecto\": \"012600038300000\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. 15 AULAS Y TINGLADO U.E. TECNICO HUMANISTICO ALVARO GARCIA <mark>LINERA</mark>-COPALANI"}]},{"document":{"gestion":2017,"devengado":332310.79,"is_class":false,"class_":"programa_proyecto","texto":"CONST. EDIFICIO MULTIFUNCIONAL MUNICIPAL LIC. ALVARO GARCIA LINERA - OMEREQUE","metadatos":"{\"entidad\": 1309, \"programa\": 17, \"proyecto\": \"3\", \"actividad\": 0.0}"},"highlights":[{"field":"texto","matched_tokens":["LINERA"],"snippet":"CONST. EDIFICIO MULTIFUNCIONAL MUNICIPAL LIC. ALVARO GARCIA <mark>LINERA</mark> - OMEREQUE"}]}]} | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -174,12 +174,9 @@ | ... | @@ -174,12 +174,9 @@ |
| 174 | align-items: center; | 174 | align-items: center; |
| 175 | gap: 0.25rem; | 175 | gap: 0.25rem; |
| 176 | padding: 6px; | 176 | padding: 6px; |
| 177 | - background: rgba(40, 40, 38, 0.7); | 177 | + background: transparent; |
| 178 | - backdrop-filter: blur(20px) saturate(180%); | 178 | + border: none; |
| 179 | - -webkit-backdrop-filter: blur(20px) saturate(180%); | ||
| 180 | - border: 1px solid rgba(255, 255, 255, 0.1); | ||
| 181 | border-radius: 14px; | 179 | border-radius: 14px; |
| 182 | - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); | ||
| 183 | } | 180 | } |
| 184 | 181 | ||
| 185 | .navbar-desktop { | 182 | .navbar-desktop { |
| ... | @@ -189,19 +186,15 @@ | ... | @@ -189,19 +186,15 @@ |
| 189 | right: 1.5rem; | 186 | right: 1.5rem; |
| 190 | z-index: 110; | 187 | z-index: 110; |
| 191 | padding: 6px; | 188 | padding: 6px; |
| 192 | - background: rgba(40, 40, 38, 0.7); | 189 | + background: transparent; |
| 193 | - backdrop-filter: blur(20px) saturate(180%); | 190 | + border: none; |
| 194 | - -webkit-backdrop-filter: blur(20px) saturate(180%); | ||
| 195 | - border: 1px solid rgba(255, 255, 255, 0.1); | ||
| 196 | border-radius: 14px; | 191 | border-radius: 14px; |
| 197 | - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); | ||
| 198 | } | 192 | } |
| 199 | 193 | ||
| 200 | :global(html:not(.dark)) .navbar-mobile, | 194 | :global(html:not(.dark)) .navbar-mobile, |
| 201 | :global(html:not(.dark)) .navbar-desktop { | 195 | :global(html:not(.dark)) .navbar-desktop { |
| 202 | - background: rgba(255, 255, 255, 0.7); | 196 | + background: transparent; |
| 203 | - border-color: rgba(0, 0, 0, 0.08); | 197 | + border: none; |
| 204 | - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); | ||
| 205 | } | 198 | } |
| 206 | 199 | ||
| 207 | .navbar-desktop-inner { | 200 | .navbar-desktop-inner { | ... | ... |
| ... | @@ -10,8 +10,8 @@ | ... | @@ -10,8 +10,8 @@ |
| 10 | 10 | ||
| 11 | let { children } = $props(); | 11 | let { children } = $props(); |
| 12 | 12 | ||
| 13 | - // Don't show navbar on landing page | 13 | + // Don't show navbar on landing pages |
| 14 | - let isLanding = $derived($page.url.pathname === '/'); | 14 | + let isLanding = $derived($page.url.pathname === '/' || $page.url.pathname === '/nuevo-buscador'); |
| 15 | 15 | ||
| 16 | // Global search modal state | 16 | // Global search modal state |
| 17 | let searchModalOpen = $state(false); | 17 | let searchModalOpen = $state(false); | ... | ... |
| 1 | <script> | 1 | <script> |
| 2 | import { onMount } from 'svelte'; | 2 | import { onMount } from 'svelte'; |
| 3 | - import { goto } from '$app/navigation'; | ||
| 4 | - import { supabase } from '$lib/supabase'; | ||
| 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 | - // Pre-warm Supabase connection (evita cold start al navegar) | ||
| 20 | - async function warmupSupabase() { | ||
| 21 | - console.time('[WARMUP] Supabase'); | ||
| 22 | - await supabase.schema('ppto').from('vista_objeto_estado').select('objeto').limit(1); | ||
| 23 | - console.timeEnd('[WARMUP] Supabase'); | ||
| 24 | - } | ||
| 25 | 3 | ||
| 4 | + // Estado base | ||
| 26 | let mounted = false; | 5 | let mounted = false; |
| 27 | - let mode = 'archivo'; | ||
| 28 | let searchFocused = false; | 6 | let searchFocused = false; |
| 29 | let searchVal = ''; | 7 | let searchVal = ''; |
| 30 | - let hDiario = false; | ||
| 31 | - let hArchivo = false; | ||
| 32 | - let hVis = null; | ||
| 33 | - let hHist = null; | ||
| 34 | - let hWidget = null; | ||
| 35 | - let hGear = false; | ||
| 36 | - let mobileSettingsOpen = false; | ||
| 37 | - let gearMenuOpen = false; | ||
| 38 | let searchInput; | 8 | let searchInput; |
| 39 | - let debounceTimer; | 9 | + let isLoading = false; |
| 40 | - let autoFocused = false; | 10 | + let results = null; |
| 41 | - | 11 | + let error = null; |
| 42 | - // Search filters - which types to include | 12 | + |
| 43 | - let searchFilters = { | 13 | + // Ordenamiento |
| 44 | - entidad: true, | 14 | + let sortBy = 'amount'; |
| 45 | - objeto_gasto: true, | 15 | + let sortOrder = 'desc'; |
| 46 | - rubro: true | 16 | + |
| 47 | - }; | 17 | + // Paginación |
| 48 | - | 18 | + let currentPage = 1; |
| 49 | - // Filtered results based on selected filters | 19 | + let isLoadingMore = false; |
| 50 | - $: filteredResults = $results.filter(item => searchFilters[item.tipo]); | 20 | + let allHits = []; |
| 51 | - | 21 | + |
| 52 | - function toggleFilter(tipo) { | 22 | + // Tracking para evitar re-animaciones |
| 53 | - searchFilters[tipo] = !searchFilters[tipo]; | 23 | + let seenHitIds = new Set(); |
| 54 | - searchFilters = searchFilters; // trigger reactivity | 24 | + let lastSearchKey = ''; |
| 25 | + | ||
| 26 | + function getHitKey(hit, index) { | ||
| 27 | + const doc = hit.document; | ||
| 28 | + const m = parseMetadatos(doc.metadatos); | ||
| 29 | + | ||
| 30 | + // Key basado en contenido + índice como tiebreaker para evitar duplicados | ||
| 31 | + if (doc.is_class === true || doc.is_class === 'true') { | ||
| 32 | + return `c-${m.entidad || 'x'}-${doc.devengado || 0}-${index}`; | ||
| 33 | + } | ||
| 34 | + return `p-${m.entidad || 0}-${m.programa || 0}-${m.proyecto || 0}-${doc.gestion || 0}-${index}`; | ||
| 55 | } | 35 | } |
| 56 | 36 | ||
| 57 | - function toggleGearMenu() { | 37 | + function isNewHit(hit) { |
| 58 | - gearMenuOpen = !gearMenuOpen; | 38 | + const key = getHitKey(hit); |
| 39 | + if (seenHitIds.has(key)) return false; | ||
| 40 | + seenHitIds.add(key); | ||
| 41 | + return true; | ||
| 59 | } | 42 | } |
| 60 | 43 | ||
| 61 | - onMount(() => { | 44 | + function resetSeenHits() { |
| 62 | - setTimeout(() => { mounted = true; }, 80); | 45 | + seenHitIds = new Set(); |
| 63 | - initIndex(); | ||
| 64 | - // Pre-calentar Supabase en background para evitar cold start | ||
| 65 | - warmupSupabase(); | ||
| 66 | - | ||
| 67 | - // Cerrar gear menu al hacer click fuera | ||
| 68 | - const handleClickOutside = (e) => { | ||
| 69 | - if (gearMenuOpen && !e.target.closest('.gear-wrap')) { | ||
| 70 | - gearMenuOpen = false; | ||
| 71 | } | 46 | } |
| 72 | - }; | ||
| 73 | - document.addEventListener('click', handleClickOutside); | ||
| 74 | - return () => document.removeEventListener('click', handleClickOutside); | ||
| 75 | - }); | ||
| 76 | 47 | ||
| 77 | - // Auto-focus en el buscador cuando el índice esté listo (solo desktop) | 48 | + function toggleSort(field) { |
| 78 | - $: if ($indexLoaded && searchInput && !autoFocused) { | 49 | + if (sortBy === field) { |
| 79 | - autoFocused = true; | 50 | + sortOrder = sortOrder === 'desc' ? 'asc' : 'desc'; |
| 80 | - // No auto-focus en móvil para evitar que aparezca el teclado | 51 | + } else { |
| 81 | - const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; | 52 | + sortBy = field; |
| 82 | - if (!isMobile) { | 53 | + sortOrder = 'desc'; |
| 83 | - setTimeout(() => { | ||
| 84 | - searchInput?.focus(); | ||
| 85 | - }, 50); | ||
| 86 | } | 54 | } |
| 87 | } | 55 | } |
| 88 | 56 | ||
| 89 | - function toggleMobileSettings() { | 57 | + $: sortedHits = allHits.length > 0 ? [...allHits].sort((a, b) => { |
| 90 | - mobileSettingsOpen = !mobileSettingsOpen; | 58 | + let valA = sortBy === 'year' ? (a.document.gestion || 0) : (a.document.devengado || 0); |
| 59 | + let valB = sortBy === 'year' ? (b.document.gestion || 0) : (b.document.devengado || 0); | ||
| 60 | + return sortOrder === 'desc' ? valB - valA : valA - valB; | ||
| 61 | + }) : []; | ||
| 62 | + | ||
| 63 | + // ¿Es búsqueda de entidades? | ||
| 64 | + $: isEntidadSearchActive = results?.hits?.[0]?.document?.class_ === 'entidad'; | ||
| 65 | + | ||
| 66 | + // Agrupar por subarea cuando es búsqueda de entidades | ||
| 67 | + $: groupedHits = (() => { | ||
| 68 | + if (!isEntidadSearchActive || allHits.length === 0) return null; | ||
| 69 | + | ||
| 70 | + const groups = {}; | ||
| 71 | + allHits.forEach(hit => { | ||
| 72 | + const m = parseMetadatos(hit.document.metadatos); | ||
| 73 | + const subarea = extractAfterHyphen(m.entidad_desc_subarea) || 'Otros'; | ||
| 74 | + if (!groups[subarea]) { | ||
| 75 | + groups[subarea] = { name: subarea, hits: [], totalMonto: 0 }; | ||
| 91 | } | 76 | } |
| 77 | + groups[subarea].hits.push(hit); | ||
| 78 | + groups[subarea].totalMonto += hit.document.devengado || 0; | ||
| 79 | + }); | ||
| 80 | + | ||
| 81 | + // Ordenar items dentro de cada grupo | ||
| 82 | + Object.values(groups).forEach(group => { | ||
| 83 | + group.hits.sort((a, b) => { | ||
| 84 | + const valA = sortBy === 'year' ? (a.document.gestion || 0) : (a.document.devengado || 0); | ||
| 85 | + const valB = sortBy === 'year' ? (b.document.gestion || 0) : (b.document.devengado || 0); | ||
| 86 | + return sortOrder === 'desc' ? valB - valA : valA - valB; | ||
| 87 | + }); | ||
| 88 | + }); | ||
| 89 | + | ||
| 90 | + // Convertir a array y ordenar grupos por monto total | ||
| 91 | + return Object.values(groups).sort((a, b) => b.totalMonto - a.totalMonto); | ||
| 92 | + })(); | ||
| 93 | + | ||
| 94 | + $: hasMoreResults = results && allHits.length < results.found; | ||
| 92 | 95 | ||
| 93 | - $: isD = mode === 'diario'; | 96 | + // Modo de búsqueda |
| 94 | - $: accent = isD ? '#4A8B6E' : '#C9A751'; | 97 | + let searchMode = 'gastos'; // 'gastos' | 'ingresos' |
| 95 | - $: placeholder = isD | 98 | + let searchSubmode = 'programas'; // 'programas' | 'clasificadores' |
| 96 | - ? 'Busca una entidad, un ministerio, un municipio...' | 99 | + let selectedClassifiers = []; |
| 97 | - : 'Busca instituciones, gastos, ingresos, sectores...'; | 100 | + |
| 98 | - $: examples = isD | 101 | + const CLASIFICADORES_GASTOS = [ |
| 99 | - ? [ | 102 | + { id: 'finfun', label: 'Finalidad y función' }, |
| 100 | - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, | 103 | + { id: 'objeto', label: 'Objetos de gasto' }, |
| 101 | - { label: 'Min. Educación', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, | 104 | + { id: 'acteco', label: 'Actividad económica' }, |
| 102 | - { label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, | 105 | + { id: 'entidad', label: 'Entidades' }, |
| 103 | - ] | 106 | + { id: 'geografico', label: 'Geográfico', disabled: true }, |
| 104 | - : [ | ||
| 105 | - { label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962', href: '/objeto/11700' }, | ||
| 106 | - { label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0', href: '/rubro/14000' }, | ||
| 107 | - { label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, | ||
| 108 | - { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, | ||
| 109 | - { label: 'Publicidad', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, | ||
| 110 | ]; | 107 | ]; |
| 111 | 108 | ||
| 112 | - function switchMode(m) { | 109 | + const CLASIFICADORES_INGRESOS = [ |
| 113 | - mode = m; | 110 | + { id: 'rubro', label: 'Rubro', disabled: true }, |
| 114 | - searchVal = ''; | 111 | + { id: 'organismo', label: 'Organismo', disabled: true }, |
| 115 | - clearSearch(); | 112 | + { id: 'fuente', label: 'Fuente', disabled: true }, |
| 116 | - if (searchInput) searchInput.value = ''; | 113 | + { id: 'entidad', label: 'Entidades' }, |
| 117 | - } | 114 | + { id: 'geografico',label: 'Geográfico', disabled: true }, |
| 115 | + ]; | ||
| 118 | 116 | ||
| 119 | - function handleBlur() { | 117 | + $: activeClasificadores = searchMode === 'gastos' ? CLASIFICADORES_GASTOS : CLASIFICADORES_INGRESOS; |
| 120 | - setTimeout(() => { searchFocused = false; }, 200); | ||
| 121 | - } | ||
| 122 | 118 | ||
| 123 | - function handleInput(e) { | 119 | + function toggleClassifier(id) { |
| 124 | - const value = e.target.value; | 120 | + if (selectedClassifiers.includes(id)) { |
| 125 | - searchVal = value; | 121 | + selectedClassifiers = selectedClassifiers.filter(c => c !== id); |
| 126 | - clearTimeout(debounceTimer); | 122 | + } else { |
| 127 | - debounceTimer = setTimeout(() => { | 123 | + selectedClassifiers = [...selectedClassifiers, id]; |
| 128 | - performSearch(value); | ||
| 129 | - }, 250); | ||
| 130 | } | 124 | } |
| 131 | - | ||
| 132 | - function handleKeydown(e) { | ||
| 133 | - if (!$showResults) return; | ||
| 134 | - switch (e.key) { | ||
| 135 | - case 'ArrowDown': | ||
| 136 | - e.preventDefault(); | ||
| 137 | - navigateResults('down', filteredResults.length); | ||
| 138 | - break; | ||
| 139 | - case 'ArrowUp': | ||
| 140 | - e.preventDefault(); | ||
| 141 | - navigateResults('up', filteredResults.length); | ||
| 142 | - break; | ||
| 143 | - case 'Enter': | ||
| 144 | - e.preventDefault(); | ||
| 145 | - // Si hay un resultado seleccionado, navegar a él | ||
| 146 | - if ($selectedIndex >= 0 && filteredResults[$selectedIndex]) { | ||
| 147 | - handleSelect(filteredResults[$selectedIndex]); | ||
| 148 | - } else if (filteredResults.length > 0) { | ||
| 149 | - // Si no hay selección pero hay resultados, ir al primero | ||
| 150 | - handleSelect(filteredResults[0]); | ||
| 151 | } | 125 | } |
| 152 | - break; | 126 | + |
| 153 | - case 'Escape': | 127 | + function setMode(mode) { |
| 154 | - clearSearch(); | 128 | + searchMode = mode; |
| 155 | - searchVal = ''; | 129 | + selectedClassifiers = []; |
| 156 | - if (searchInput) searchInput.value = ''; | 130 | + if (mode === 'gastos') { |
| 157 | - searchInput?.blur(); | 131 | + searchSubmode = 'programas'; |
| 158 | - break; | 132 | + } else { |
| 133 | + searchSubmode = 'clasificadores'; | ||
| 159 | } | 134 | } |
| 160 | } | 135 | } |
| 161 | 136 | ||
| 162 | - function handleSelect(item) { | 137 | + function setSubmode(sub) { |
| 163 | - if (item.tipo === 'entidad') { | 138 | + searchSubmode = sub; |
| 164 | - goto(`/entidad/${item.codigo}`); | 139 | + selectedClassifiers = []; |
| 165 | - } else if (item.tipo === 'objeto_gasto') { | ||
| 166 | - goto(`/objeto/${item.codigo}`); | ||
| 167 | - } else if (item.tipo === 'rubro') { | ||
| 168 | - goto(`/rubro/${item.codigo}`); | ||
| 169 | - } | ||
| 170 | - clearSearch(); | ||
| 171 | - searchVal = ''; | ||
| 172 | - if (searchInput) searchInput.value = ''; | ||
| 173 | } | 140 | } |
| 174 | 141 | ||
| 175 | - function getTypeLabel(tipo) { | 142 | + // Placeholder dinámico |
| 176 | - switch (tipo) { | 143 | + $: placeholderText = getPlaceholderText(searchMode, searchSubmode, selectedClassifiers); |
| 177 | - case 'entidad': return 'ENTIDAD'; | 144 | + |
| 178 | - case 'objeto_gasto': return 'OBJETO'; | 145 | + function getPlaceholderText(mode, submode, classifiers) { |
| 179 | - case 'rubro': return 'RUBRO'; | 146 | + if (mode === 'gastos') { |
| 180 | - default: return tipo?.toUpperCase() || ''; | 147 | + if (submode === 'programas') return 'Buscar gastos en programas y proyectos...'; |
| 148 | + if (classifiers.length === 0) return 'Selecciona al menos un clasificador...'; | ||
| 149 | + const nombres = classifiers.map(id => { | ||
| 150 | + const clf = CLASIFICADORES_GASTOS.find(c => c.id === id); | ||
| 151 | + return clf?.label?.toLowerCase() || id; | ||
| 152 | + }).join(', '); | ||
| 153 | + return `Buscar gastos en ${nombres}...`; | ||
| 154 | + } else { | ||
| 155 | + if (classifiers.length === 0) return 'Selecciona al menos un clasificador...'; | ||
| 156 | + const nombres = classifiers.map(id => { | ||
| 157 | + const clf = CLASIFICADORES_INGRESOS.find(c => c.id === id); | ||
| 158 | + return clf?.label?.toLowerCase() || id; | ||
| 159 | + }).join(', '); | ||
| 160 | + return `Buscar ingresos en ${nombres}...`; | ||
| 181 | } | 161 | } |
| 182 | } | 162 | } |
| 183 | 163 | ||
| 184 | - function getTypeColor(tipo) { | 164 | + // Debounce |
| 185 | - switch (tipo) { | 165 | + let debounceTimer; |
| 186 | - case 'entidad': return '#4A8B6E'; | 166 | + let hintTimer; |
| 187 | - case 'objeto_gasto': return '#C9A751'; | 167 | + let showMinLengthHint = false; |
| 188 | - case 'rubro': return '#8B6E9B'; | 168 | + const DEBOUNCE_MS = 450; |
| 189 | - default: return '#6B6860'; | 169 | + const HINT_DELAY_MS = 1500; |
| 170 | + | ||
| 171 | + const POBLACION = 12000000; | ||
| 172 | + const API_BASE = '/api/search'; | ||
| 173 | + | ||
| 174 | + // Hint diario | ||
| 175 | + let dailyData = null; | ||
| 176 | + let dailyLoading = true; | ||
| 177 | + | ||
| 178 | + async function fetchDaily() { | ||
| 179 | + try { | ||
| 180 | + const res = await fetch('/api/daily-summary'); | ||
| 181 | + if (res.ok) dailyData = await res.json(); | ||
| 182 | + } catch { | ||
| 183 | + // silencioso — es un hint, no crítico | ||
| 184 | + } finally { | ||
| 185 | + dailyLoading = false; | ||
| 190 | } | 186 | } |
| 191 | } | 187 | } |
| 192 | 188 | ||
| 189 | + // Ejemplos | ||
| 190 | + const EXAMPLES_PROGRAMAS = [ | ||
| 191 | + { label: 'Vía férrea Bulo Bulo', color: 'rgba(232,146,124,0.22)', textColor: '#E8927C' }, | ||
| 192 | + { label: 'Siderúrgica del Mutún', color: 'rgba(160,165,185,0.18)', textColor: '#B8BDCC' }, | ||
| 193 | + ]; | ||
| 194 | + | ||
| 195 | + const EXAMPLES_ENTIDADES = [ | ||
| 196 | + { label: 'Fondo Indígena', color: 'rgba(126,184,216,0.22)', textColor: '#8EC8E8' }, | ||
| 197 | + { label: 'YPFB', color: 'rgba(168,196,160,0.22)', textColor: '#B8D4B0' }, | ||
| 198 | + ]; | ||
| 199 | + | ||
| 200 | + // Ejemplos dinámicos según selección | ||
| 201 | + $: currentExamples = (selectedClassifiers.includes('entidad') && selectedClassifiers.length === 1) | ||
| 202 | + ? EXAMPLES_ENTIDADES | ||
| 203 | + : EXAMPLES_PROGRAMAS; | ||
| 204 | + | ||
| 205 | + // Datos para secciones | ||
| 193 | const WIDGETS_INGRESOS = [ | 206 | const WIDGETS_INGRESOS = [ |
| 194 | { id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' }, | 207 | { id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' }, |
| 195 | - { id: 'fuentes', label: 'fuentes de financiamiento', d: 'TGN, crédito externo, donaciones', t: 'Fuentes', n: '22', href: '/clasificadores/fuente' }, | 208 | + { id: 'fuentes', label: 'fuentes de financiamiento', d: 'TGN, crédito externo, donaciones', t: 'Fuentes', n: '22', href: '/clasificadores/fuentes' }, |
| 196 | { id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' }, | 209 | { id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' }, |
| 197 | ]; | 210 | ]; |
| 198 | 211 | ||
| ... | @@ -201,25 +214,27 @@ | ... | @@ -201,25 +214,27 @@ |
| 201 | { id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' }, | 214 | { id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' }, |
| 202 | ]; | 215 | ]; |
| 203 | 216 | ||
| 204 | - const WIDGETS_GASTOS_BOTTOM = [ | ||
| 205 | - { id: 'finalidad', label: 'finalidades y funciones', d: 'Educación, salud, defensa', t: 'Finalidad', n: '132', href: '/clasificadores/funcional' }, | ||
| 206 | - { id: 'donde', label: 'ubicaciones geográficas', d: 'Departamentos, municipios', t: 'Geográfico', n: '352', href: '/clasificadores/geografico' }, | ||
| 207 | - ]; | ||
| 208 | - | ||
| 209 | const HISTORIAS = [ | 217 | const HISTORIAS = [ |
| 210 | - { color: '#C9A751', title: '¿Cuánto gasta Bolivia en consultorías cada año?', desc: 'Un análisis de 20 años revela patrones que todo ciudadano debería conocer. La evolución del gasto en servicios profesionales muestra tendencias inesperadas.', tag: 'INVESTIGACIÓN', tall: true }, | 218 | + { color: '#C9A751', title: '¿Cuánto gasta Bolivia en consultorías cada año?', desc: 'Un análisis de 20 años revela patrones que todo ciudadano debería conocer.', tag: 'INVESTIGACIÓN', tall: true }, |
| 211 | { color: '#C44B3F', title: 'Salud vs. Defensa: 20 años', desc: 'Cómo se redistribuyó el presupuesto entre sectores.', tag: 'COMPARATIVA', tall: false }, | 219 | { color: '#C44B3F', title: 'Salud vs. Defensa: 20 años', desc: 'Cómo se redistribuyó el presupuesto entre sectores.', tag: 'COMPARATIVA', tall: false }, |
| 212 | - { color: '#4A8B6E', title: 'El boom del gasto en infraestructura', desc: 'De 2006 a 2014 Bolivia multiplicó por 8 su inversión pública. Un recorrido por las grandes obras y su impacto fiscal.', tag: 'CRÓNICA', tall: true }, | 220 | + { color: '#4A8B6E', title: 'El boom del gasto en infraestructura', desc: 'De 2006 a 2014 Bolivia multiplicó por 8 su inversión pública.', tag: 'CRÓNICA', tall: true }, |
| 213 | { color: '#6B8CA6', title: '¿Quién financia a Bolivia?', desc: 'Un mapa de los organismos internacionales que prestan al país.', tag: 'ANÁLISIS', tall: false }, | 221 | { color: '#6B8CA6', title: '¿Quién financia a Bolivia?', desc: 'Un mapa de los organismos internacionales que prestan al país.', tag: 'ANÁLISIS', tall: false }, |
| 214 | ]; | 222 | ]; |
| 215 | 223 | ||
| 216 | const VISUALIZACIONES = [ | 224 | const VISUALIZACIONES = [ |
| 217 | - { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades en salud a lo largo de dos décadas.', type: 'Serie temporal', href: null }, | 225 | + { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades.', type: 'Serie temporal', href: null }, |
| 218 | - { color: '#C9A751', title: '¿En qué se gasta?', desc: 'Explora el presupuesto por objeto de gasto.', type: 'Treemap', href: '/clasificadores/objeto-gasto?modo=mapa' }, | 226 | + { color: '#C9A751', title: '¿En qué se gasta?', desc: 'Explora el presupuesto por objeto de gasto.', type: 'Treemap', href: '/clasificadores/objeto-gasto' }, |
| 219 | - { color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa', href: null }, | 227 | + { color: '#8B6E9B', title: 'Mapa de inversión', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa', href: null }, |
| 220 | ]; | 228 | ]; |
| 221 | 229 | ||
| 222 | - /* Reveal action */ | 230 | + // Hover states para secciones |
| 231 | + let hDiario = false; | ||
| 232 | + let hArchivo = false; | ||
| 233 | + let hWidget = null; | ||
| 234 | + let hHist = null; | ||
| 235 | + let hVis = null; | ||
| 236 | + | ||
| 237 | + // Reveal animation | ||
| 223 | let revealStates = {}; | 238 | let revealStates = {}; |
| 224 | function reveal(node, { id, delay = 0 }) { | 239 | function reveal(node, { id, delay = 0 }) { |
| 225 | revealStates[id] = false; | 240 | revealStates[id] = false; |
| ... | @@ -234,7 +249,7 @@ | ... | @@ -234,7 +249,7 @@ |
| 234 | return { destroy() { obs.disconnect(); } }; | 249 | return { destroy() { obs.disconnect(); } }; |
| 235 | } | 250 | } |
| 236 | 251 | ||
| 237 | - /* Section navigation */ | 252 | + // Navegación interna - scroll a secciones |
| 238 | const SECTIONS_NAV = [ | 253 | const SECTIONS_NAV = [ |
| 239 | { id: 'hero', label: 'Inicio' }, | 254 | { id: 'hero', label: 'Inicio' }, |
| 240 | { id: 'datos', label: 'Datos' }, | 255 | { id: 'datos', label: 'Datos' }, |
| ... | @@ -246,28 +261,231 @@ | ... | @@ -246,28 +261,231 @@ |
| 246 | ]; | 261 | ]; |
| 247 | let currentSection = 'hero'; | 262 | let currentSection = 'hero'; |
| 248 | 263 | ||
| 264 | + function scrollToSection(id) { | ||
| 265 | + const el = document.querySelector(`[data-section="${id}"]`); | ||
| 266 | + if (el) el.scrollIntoView({ behavior: 'smooth' }); | ||
| 267 | + } | ||
| 268 | + | ||
| 269 | + // Observer para actualizar sección activa | ||
| 270 | + let sectionObserver; | ||
| 249 | onMount(() => { | 271 | onMount(() => { |
| 272 | + setTimeout(() => { mounted = true; }, 80); | ||
| 273 | + fetchDaily(); | ||
| 274 | + | ||
| 275 | + const isMobile = window.innerWidth < 768; | ||
| 276 | + if (!isMobile) setTimeout(() => searchInput?.focus(), 300); | ||
| 277 | + | ||
| 278 | + // IntersectionObserver para secciones | ||
| 250 | const sections = document.querySelectorAll('[data-section]'); | 279 | const sections = document.querySelectorAll('[data-section]'); |
| 251 | - const observer = new IntersectionObserver((entries) => { | 280 | + sectionObserver = new IntersectionObserver((entries) => { |
| 252 | entries.forEach(entry => { | 281 | entries.forEach(entry => { |
| 253 | if (entry.isIntersecting) { | 282 | if (entry.isIntersecting) { |
| 254 | currentSection = entry.target.dataset.section; | 283 | currentSection = entry.target.dataset.section; |
| 255 | } | 284 | } |
| 256 | }); | 285 | }); |
| 257 | - }, { threshold: 0.5 }); | 286 | + }, { threshold: 0.3 }); |
| 258 | - sections.forEach(section => observer.observe(section)); | 287 | + sections.forEach(section => sectionObserver.observe(section)); |
| 259 | - return () => observer.disconnect(); | 288 | + |
| 289 | + return () => sectionObserver?.disconnect(); | ||
| 260 | }); | 290 | }); |
| 261 | 291 | ||
| 262 | - function scrollToSection(id) { | 292 | + function searchExample(label) { |
| 263 | - const el = document.querySelector(`[data-section="${id}"]`); | 293 | + searchVal = label; |
| 264 | - if (el) el.scrollIntoView({ behavior: 'smooth' }); | 294 | + performSearch(label); |
| 295 | + searchInput?.focus(); | ||
| 296 | + } | ||
| 297 | + | ||
| 298 | + async function handleInput(e) { | ||
| 299 | + searchVal = e.target.value; | ||
| 300 | + clearTimeout(debounceTimer); | ||
| 301 | + clearTimeout(hintTimer); | ||
| 302 | + showMinLengthHint = false; | ||
| 303 | + | ||
| 304 | + if (searchVal.length < 3) { | ||
| 305 | + results = null; | ||
| 306 | + error = null; | ||
| 307 | + if (searchVal.length > 0) { | ||
| 308 | + hintTimer = setTimeout(() => { showMinLengthHint = true; }, HINT_DELAY_MS); | ||
| 309 | + } | ||
| 310 | + return; | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + const needsClassifiers = searchMode === 'ingresos' || | ||
| 314 | + (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 315 | + if (needsClassifiers && selectedClassifiers.length === 0) { | ||
| 316 | + results = null; | ||
| 317 | + error = null; | ||
| 318 | + return; | ||
| 319 | + } | ||
| 320 | + | ||
| 321 | + debounceTimer = setTimeout(() => performSearch(searchVal), DEBOUNCE_MS); | ||
| 322 | + } | ||
| 323 | + | ||
| 324 | + async function performSearch(query, page = 1) { | ||
| 325 | + // NO borrar allHits inmediatamente - mantener resultados anteriores visibles | ||
| 326 | + if (page === 1) { | ||
| 327 | + isLoading = true; | ||
| 328 | + // Solo resetear sort en nueva búsqueda | ||
| 329 | + sortBy = 'amount'; | ||
| 330 | + sortOrder = 'desc'; | ||
| 331 | + } else { | ||
| 332 | + isLoadingMore = true; | ||
| 333 | + } | ||
| 334 | + error = null; | ||
| 335 | + currentPage = page; | ||
| 336 | + | ||
| 337 | + try { | ||
| 338 | + const isClassSearch = searchMode === 'ingresos' || | ||
| 339 | + (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 340 | + const params = new URLSearchParams({ | ||
| 341 | + q: query, | ||
| 342 | + is_class: isClassSearch ? 'true' : 'false', | ||
| 343 | + mode: searchMode, | ||
| 344 | + page: page.toString() | ||
| 345 | + }); | ||
| 346 | + // Para clasificadores, pedir más resultados para tener conteos precisos | ||
| 347 | + if (isClassSearch) { | ||
| 348 | + params.set('per_page', '250'); | ||
| 349 | + } | ||
| 350 | + if (isClassSearch && selectedClassifiers.length > 0) { | ||
| 351 | + params.set('class_', selectedClassifiers.join(',')); | ||
| 352 | + } | ||
| 353 | + | ||
| 354 | + console.log('[Search]', `${API_BASE}?${params}`); | ||
| 355 | + const res = await fetch(`${API_BASE}?${params}`); | ||
| 356 | + if (!res.ok) throw new Error(`Error ${res.status}`); | ||
| 357 | + const data = await res.json(); | ||
| 358 | + | ||
| 359 | + // Actualizar resultados DESPUÉS de recibir datos (no antes) | ||
| 360 | + results = data; | ||
| 361 | + if (page === 1) { | ||
| 362 | + resetSeenHits(); | ||
| 363 | + allHits = data.hits; | ||
| 364 | + } else { | ||
| 365 | + allHits = [...allHits, ...data.hits]; | ||
| 366 | + } | ||
| 367 | + } catch (err) { | ||
| 368 | + console.error(err); | ||
| 369 | + error = 'Error al buscar. Intenta de nuevo.'; | ||
| 370 | + if (page === 1) { results = null; allHits = []; } | ||
| 371 | + } | ||
| 372 | + | ||
| 373 | + isLoading = false; | ||
| 374 | + isLoadingMore = false; | ||
| 375 | + } | ||
| 376 | + | ||
| 377 | + function loadMore() { | ||
| 378 | + if (hasMoreResults && !isLoadingMore) performSearch(searchVal, currentPage + 1); | ||
| 379 | + } | ||
| 380 | + | ||
| 381 | + function handleKeydown(e) { | ||
| 382 | + if (e.key === 'Escape') { clearSearch(); searchInput?.blur(); } | ||
| 265 | } | 383 | } |
| 384 | + | ||
| 385 | + function clearSearch() { | ||
| 386 | + searchVal = ''; | ||
| 387 | + results = null; | ||
| 388 | + allHits = []; | ||
| 389 | + currentPage = 1; | ||
| 390 | + error = null; | ||
| 391 | + showMinLengthHint = false; | ||
| 392 | + clearTimeout(debounceTimer); | ||
| 393 | + clearTimeout(hintTimer); | ||
| 394 | + searchInput?.focus(); | ||
| 395 | + } | ||
| 396 | + | ||
| 397 | + // Helpers | ||
| 398 | + function extractAfterHyphen(text) { | ||
| 399 | + if (!text) return ''; | ||
| 400 | + const parts = text.split(' - '); | ||
| 401 | + return parts.length > 1 ? parts.slice(1).join(' - ').trim() : text; | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + function parseMetadatos(meta) { | ||
| 405 | + if (!meta) return {}; | ||
| 406 | + if (typeof meta === 'object') return meta; | ||
| 407 | + try { return JSON.parse(meta); } catch { return {}; } | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + // Usar allHits para stats (no solo la última página) | ||
| 411 | + function getStats(hits, totalFound) { | ||
| 412 | + if (!hits?.length) return null; | ||
| 413 | + const isClassifier = hits[0]?.document?.is_class === true; | ||
| 414 | + const isEntidad = hits[0]?.document?.class_ === 'entidad'; | ||
| 415 | + const montoTotal = hits.reduce((s, h) => s + (h.document.devengado || 0), 0); | ||
| 416 | + | ||
| 417 | + if (isClassifier) { | ||
| 418 | + const entidadesSet = new Set(); | ||
| 419 | + hits.forEach(h => { | ||
| 420 | + const m = parseMetadatos(h.document.metadatos); | ||
| 421 | + if (m.entidad) entidadesSet.add(m.entidad); | ||
| 422 | + }); | ||
| 423 | + return { found: totalFound, montoTotal, perCapita: montoTotal / POBLACION, | ||
| 424 | + numEntidades: entidadesSet.size, | ||
| 425 | + isClassifier: true, isEntidad, classType: hits[0]?.document?.class_ || null }; | ||
| 426 | + } else { | ||
| 427 | + const entidadesSet = new Set(); | ||
| 428 | + hits.forEach(h => { const m = parseMetadatos(h.document.metadatos); if (m.entidad) entidadesSet.add(m.entidad); }); | ||
| 429 | + const years = hits.map(h => h.document.gestion).filter(Boolean); | ||
| 430 | + const minYear = Math.min(...years); | ||
| 431 | + const maxYear = Math.max(...years); | ||
| 432 | + return { found: totalFound, montoTotal, perCapita: montoTotal / POBLACION, | ||
| 433 | + numEntidades: entidadesSet.size, | ||
| 434 | + yearRange: minYear === maxYear ? `${minYear}` : `${minYear} – ${maxYear}`, | ||
| 435 | + isClassifier: false }; | ||
| 436 | + } | ||
| 437 | + } | ||
| 438 | + | ||
| 439 | + function formatMonto(num) { | ||
| 440 | + if (num >= 1e9) return `${(num/1e9).toFixed(1)} mil millones de Bs`; | ||
| 441 | + if (num >= 1e6) return `${(num/1e6).toFixed(1)} millones de Bs`; | ||
| 442 | + if (num >= 1e3) return `${(num/1e3).toFixed(0)} mil Bs`; | ||
| 443 | + return `${num.toFixed(0)} Bs`; | ||
| 444 | + } | ||
| 445 | + | ||
| 446 | + function formatMontoFull(num) { | ||
| 447 | + return `${num.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} Bs`; | ||
| 448 | + } | ||
| 449 | + | ||
| 450 | + function generateCodigo(hit) { | ||
| 451 | + const m = parseMetadatos(hit.document.metadatos); | ||
| 452 | + return `${m.entidad}-${m.programa}-${m.proyecto}-${m.actividad}-${hit.document.gestion}`; | ||
| 453 | + } | ||
| 454 | + | ||
| 455 | + $: stats = allHits.length > 0 ? getStats(allHits, results?.found || allHits.length) : null; | ||
| 456 | + | ||
| 457 | + $: { | ||
| 458 | + const _mode = searchMode; | ||
| 459 | + const _submode = searchSubmode; | ||
| 460 | + const _classifiers = selectedClassifiers; | ||
| 461 | + if (searchVal.length >= 3 && mounted) { | ||
| 462 | + clearTimeout(debounceTimer); | ||
| 463 | + const needsClassifiers = _mode === 'ingresos' || (_mode === 'gastos' && _submode === 'clasificadores'); | ||
| 464 | + if (!needsClassifiers || _classifiers.length > 0) { | ||
| 465 | + debounceTimer = setTimeout(() => performSearch(searchVal), 150); | ||
| 466 | + } else { | ||
| 467 | + // No hay clasificadores seleccionados - limpiar resultados | ||
| 468 | + results = null; | ||
| 469 | + allHits = []; | ||
| 470 | + } | ||
| 471 | + } | ||
| 472 | + } | ||
| 473 | + | ||
| 474 | + // ¿Mostrar chips de clasificadores? | ||
| 475 | + $: showClassifierChips = searchMode === 'ingresos' || | ||
| 476 | + (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 477 | + | ||
| 478 | + // ¿Hay búsqueda activa? | ||
| 479 | + $: isSearching = searchVal.length >= 3; | ||
| 266 | </script> | 480 | </script> |
| 267 | 481 | ||
| 482 | +<svelte:head> | ||
| 483 | + <title>Buscador | Presupuesto Abierto</title> | ||
| 484 | +</svelte:head> | ||
| 485 | + | ||
| 268 | <div class="page" class:mounted> | 486 | <div class="page" class:mounted> |
| 269 | 487 | ||
| 270 | - <!-- Section Navigation Dots --> | 488 | + <!-- Dots de navegación --> |
| 271 | <nav class="section-dots"> | 489 | <nav class="section-dots"> |
| 272 | {#each SECTIONS_NAV as s} | 490 | {#each SECTIONS_NAV as s} |
| 273 | <button | 491 | <button |
| ... | @@ -281,59 +499,39 @@ | ... | @@ -281,59 +499,39 @@ |
| 281 | {/each} | 499 | {/each} |
| 282 | </nav> | 500 | </nav> |
| 283 | 501 | ||
| 284 | - <!-- ═══ LANDING SCREEN (NAV + HERO) ═══ --> | 502 | + <!-- Hero --> |
| 285 | - <section class="landing-screen" data-section="hero"> | 503 | + <main data-section="hero"> |
| 286 | - <nav> | ||
| 287 | - <div class="nav-logo"><img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /></div> | ||
| 288 | - </nav> | ||
| 289 | 504 | ||
| 290 | - <!-- ═══ HERO ═══ --> | 505 | + <div class="nav-logo"> |
| 291 | - <section class="hero"> | 506 | + <img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /> |
| 292 | - <div class="dot-pattern"></div> | ||
| 293 | - <div class="hero-inner"> | ||
| 294 | - <!-- Bloque 1: Identidad --> | ||
| 295 | - <div class="hero-identity"> | ||
| 296 | - <h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1> | ||
| 297 | - <p class="hero-sub anim" style="--d:340ms"><a href="/vista-diaria" class="hero-sub-link">Información diaria</a> e histórica de todo el Estado Boliviano.</p> | ||
| 298 | - <div class="hero-stats anim" style="--d:420ms"> | ||
| 299 | - <span class="stat-num">21</span> años<span class="stat-dot">·</span><span class="stat-num">8.3M</span> registros<span class="stat-dot">·</span><span class="stat-num">647</span> instituciones<span class="stat-dot">·</span><span class="stat-num">7</span> <a href="#clasificadores" class="stat-link">clasificadores</a><span class="stat-dot">·</span><span class="stat-num">10</span> <a href="#visualizaciones" class="stat-link">visualizaciones</a><span class="stat-dot">·</span><span class="stat-num">4</span> <a href="#historias" class="stat-link">historias</a> | ||
| 300 | - </div> | ||
| 301 | - </div> | ||
| 302 | - | ||
| 303 | - <!-- Bloque 2: Búsqueda --> | ||
| 304 | - <div class="hero-search anim" style="--d:520ms"> | ||
| 305 | - <div class="search-wrap"> | ||
| 306 | - <!-- Search bar unificado --> | ||
| 307 | - <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && $query.length >= 2 && ($results.length > 0 || $results.length === 0 || $isLoading)} style="--accent:{accent}"> | ||
| 308 | - <!-- Link al diario + Badge informativo (desktop) --> | ||
| 309 | - <div class="search-meta toggle-desktop"> | ||
| 310 | - <a href="/vista-diaria" class="search-meta-item search-diario-link"> | ||
| 311 | - Ver gasto de hoy | ||
| 312 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 313 | - <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 314 | - </svg> | ||
| 315 | - </a> | ||
| 316 | - <span class="search-meta-item search-badge"> | ||
| 317 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 318 | - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | ||
| 319 | - </svg> | ||
| 320 | - Viendo 2005 – 2025 | ||
| 321 | - </span> | ||
| 322 | </div> | 507 | </div> |
| 323 | - <div class="search-divider"></div> | ||
| 324 | 508 | ||
| 325 | - <!-- Mobile: mode indicator + settings button --> | 509 | + <button class="nav-menu-btn" aria-label="Menú"> |
| 326 | - <button class="mobile-settings-btn" class:mobile-settings-open={mobileSettingsOpen} on:click={toggleMobileSettings}> | 510 | + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> |
| 327 | - <span class="mobile-mode-indicator" style="background:{accent}"></span> | 511 | + <line x1="3" y1="6" x2="21" y2="6"/> |
| 328 | - <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={mobileSettingsOpen ? accent : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | 512 | + <line x1="3" y1="12" x2="21" y2="12"/> |
| 329 | - <circle cx="12" cy="12" r="3"/> | 513 | + <line x1="3" y1="18" x2="21" y2="18"/> |
| 330 | - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 331 | </svg> | 514 | </svg> |
| 332 | </button> | 515 | </button> |
| 333 | 516 | ||
| 334 | - <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={searchFocused ? accent : '#B8B5AD'} stroke-width="2" stroke-linecap="round"> | 517 | + <div class="search-container anim" style="--d:200ms"> |
| 518 | + | ||
| 519 | + <!-- Título --> | ||
| 520 | + <h1 class="hero-title" class:hero-collapsed={allHits.length > 0}> | ||
| 521 | + Presupuesto <span class="gold">Abierto</span> | ||
| 522 | + </h1> | ||
| 523 | + <p class="hero-sub" class:hero-collapsed={allHits.length > 0}> | ||
| 524 | + Información histórica y diaria de todo el Estado Boliviano. | ||
| 525 | + </p> | ||
| 526 | + | ||
| 527 | + <!-- Buscador --> | ||
| 528 | + <div class="search-wrap" class:search-wrap-with-results={allHits.length > 0}> | ||
| 529 | + <div class="search-bar" class:search-focused={searchFocused} class:has-results={isSearching}> | ||
| 530 | + <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" | ||
| 531 | + stroke={searchFocused ? '#D4B254' : '#8C8880'} stroke-width="2" stroke-linecap="round"> | ||
| 335 | <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> | 532 | <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> |
| 336 | </svg> | 533 | </svg> |
| 534 | + | ||
| 337 | <div class="search-input-wrap"> | 535 | <div class="search-input-wrap"> |
| 338 | <input | 536 | <input |
| 339 | type="text" | 537 | type="text" |
| ... | @@ -342,172 +540,324 @@ | ... | @@ -342,172 +540,324 @@ |
| 342 | on:input={handleInput} | 540 | on:input={handleInput} |
| 343 | on:keydown={handleKeydown} | 541 | on:keydown={handleKeydown} |
| 344 | on:focus={() => searchFocused = true} | 542 | on:focus={() => searchFocused = true} |
| 345 | - on:blur={handleBlur} | 543 | + on:blur={() => searchFocused = false} |
| 346 | - disabled={!$indexLoaded} | 544 | + placeholder="" |
| 347 | /> | 545 | /> |
| 348 | {#if !searchVal} | 546 | {#if !searchVal} |
| 349 | <div class="search-placeholder"> | 547 | <div class="search-placeholder"> |
| 350 | - <span class="blink-cursor" class:cursor-diario={isD} class:cursor-archivo={!isD}></span> | 548 | + <span class="blink-cursor" class:cursor-hidden={searchFocused}></span> |
| 351 | - <span class="placeholder-text placeholder-desktop">{placeholder}</span> | 549 | + <span class="placeholder-text">{placeholderText}</span> |
| 352 | - <span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span> | ||
| 353 | </div> | 550 | </div> |
| 354 | {/if} | 551 | {/if} |
| 355 | - {#if searchVal && !$isLoading} | 552 | + {#if searchVal && !isLoading} |
| 356 | - <button class="search-clear" on:click={() => { clearSearch(); searchVal = ''; if (searchInput) searchInput.value = ''; }}> | 553 | + <button class="search-clear" on:click={clearSearch}> |
| 357 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> | 554 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> |
| 358 | - </button> | 555 | + <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> |
| 359 | - {/if} | ||
| 360 | - </div> | ||
| 361 | - <!-- Gear button with filter dropdown (desktop) --> | ||
| 362 | - <div class="gear-wrap gear-desktop" on:click|stopPropagation> | ||
| 363 | - <button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu}> | ||
| 364 | - <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={gearMenuOpen ? accent : '#B8B5AD'} stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"> | ||
| 365 | - <circle cx="12" cy="12" r="3"/> | ||
| 366 | - <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 367 | </svg> | 556 | </svg> |
| 368 | </button> | 557 | </button> |
| 369 | - {#if gearMenuOpen} | ||
| 370 | - <div class="gear-menu"> | ||
| 371 | - <div class="gear-menu-title">Ajusta tu búsqueda</div> | ||
| 372 | - <button class="gear-row" on:click={() => toggleFilter('entidad')}> | ||
| 373 | - <div class="gear-check" class:gear-check-on={searchFilters.entidad}> | ||
| 374 | - {#if searchFilters.entidad} | ||
| 375 | - <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 376 | {/if} | 558 | {/if} |
| 377 | - </div> | 559 | + {#if isLoading} |
| 378 | - <div class="gear-row-content"> | 560 | + <div class="search-loading"><span class="loading-dot"></span></div> |
| 379 | - <span class="gear-row-question">¿Quién?</span> | ||
| 380 | - <span class="gear-row-label" class:gear-row-label-on={searchFilters.entidad}>Instituciones</span> | ||
| 381 | - </div> | ||
| 382 | - <span class="gear-row-count">756</span> | ||
| 383 | - </button> | ||
| 384 | - <button class="gear-row" on:click={() => toggleFilter('objeto_gasto')}> | ||
| 385 | - <div class="gear-check" class:gear-check-on={searchFilters.objeto_gasto}> | ||
| 386 | - {#if searchFilters.objeto_gasto} | ||
| 387 | - <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 388 | {/if} | 561 | {/if} |
| 389 | </div> | 562 | </div> |
| 390 | - <div class="gear-row-content"> | ||
| 391 | - <span class="gear-row-question">¿En qué?</span> | ||
| 392 | - <span class="gear-row-label" class:gear-row-label-on={searchFilters.objeto_gasto}>Objetos de gasto</span> | ||
| 393 | </div> | 563 | </div> |
| 394 | - <span class="gear-row-count">324</span> | 564 | + |
| 565 | + <!-- ─── FILTROS INLINE ─── --> | ||
| 566 | + <div class="filters-row" class:filters-hidden={isSearching}> | ||
| 567 | + | ||
| 568 | + <!-- Nivel 1: Segmented control Gastos / Ingresos --> | ||
| 569 | + <div class="filter-level filter-level-1"> | ||
| 570 | + <div class="segmented-control"> | ||
| 571 | + <button | ||
| 572 | + class="segment" | ||
| 573 | + class:segment-active={searchMode === 'gastos'} | ||
| 574 | + on:click={() => setMode('gastos')} | ||
| 575 | + > | ||
| 576 | + Gastos | ||
| 577 | + </button> | ||
| 578 | + <button | ||
| 579 | + class="segment" | ||
| 580 | + class:segment-active={searchMode === 'ingresos'} | ||
| 581 | + on:click={() => setMode('ingresos')} | ||
| 582 | + > | ||
| 583 | + Ingresos | ||
| 395 | </button> | 584 | </button> |
| 396 | - <button class="gear-row" on:click={() => toggleFilter('rubro')}> | ||
| 397 | - <div class="gear-check" class:gear-check-on={searchFilters.rubro}> | ||
| 398 | - {#if searchFilters.rubro} | ||
| 399 | - <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 400 | - {/if} | ||
| 401 | - </div> | ||
| 402 | - <div class="gear-row-content"> | ||
| 403 | - <span class="gear-row-question">¿De dónde?</span> | ||
| 404 | - <span class="gear-row-label" class:gear-row-label-on={searchFilters.rubro}>Rubros de ingreso</span> | ||
| 405 | </div> | 585 | </div> |
| 406 | - <span class="gear-row-count">324</span> | 586 | + |
| 587 | + <!-- Nivel 2: Segmented control Programas / Clasificadores (solo Gastos) --> | ||
| 588 | + {#if searchMode === 'gastos'} | ||
| 589 | + <div class="segmented-control segmented-control-secondary"> | ||
| 590 | + <button | ||
| 591 | + class="segment segment-secondary" | ||
| 592 | + class:segment-active={searchSubmode === 'programas'} | ||
| 593 | + on:click={() => setSubmode('programas')} | ||
| 594 | + > | ||
| 595 | + Programas y proyectos | ||
| 596 | + </button> | ||
| 597 | + <button | ||
| 598 | + class="segment segment-secondary" | ||
| 599 | + class:segment-active={searchSubmode === 'clasificadores'} | ||
| 600 | + on:click={() => setSubmode('clasificadores')} | ||
| 601 | + > | ||
| 602 | + Clasificadores | ||
| 407 | </button> | 603 | </button> |
| 408 | </div> | 604 | </div> |
| 409 | {/if} | 605 | {/if} |
| 410 | </div> | 606 | </div> |
| 411 | - </div> | ||
| 412 | 607 | ||
| 413 | - <!-- Search Results Dropdown --> | 608 | + <!-- Nivel 3: Clasificadores con checkboxes (cuando aplica) --> |
| 414 | - {#if $isLoading && $query.length >= 2} | 609 | + {#if showClassifierChips} |
| 415 | - <div class="search-results"> | 610 | + <div class="filter-level filter-level-2"> |
| 416 | - <div class="search-loading">Buscando...</div> | 611 | + <span class="filter-level-label">Selecciona clasificadores:</span> |
| 417 | - </div> | 612 | + <div class="checkbox-group"> |
| 418 | - {:else if searchFocused && filteredResults.length > 0} | 613 | + {#each activeClasificadores as clf} |
| 419 | - <div class="search-results"> | ||
| 420 | - {#each filteredResults as item, i} | ||
| 421 | <button | 614 | <button |
| 422 | - type="button" | 615 | + class="checkbox-chip" |
| 423 | - class="search-result-item" | 616 | + class:checkbox-chip-active={selectedClassifiers.includes(clf.id)} |
| 424 | - class:result-selected={$selectedIndex === i} | 617 | + class:checkbox-chip-disabled={clf.disabled} |
| 425 | - on:click={() => handleSelect(item)} | 618 | + on:click={() => { if (!clf.disabled) toggleClassifier(clf.id); }} |
| 619 | + disabled={clf.disabled} | ||
| 426 | > | 620 | > |
| 427 | - <div class="result-main"> | 621 | + <span class="checkbox-box"> |
| 428 | - <span class="result-name">{item.nombre}</span> | 622 | + {#if selectedClassifiers.includes(clf.id)} |
| 429 | - {#if item.sigla} | 623 | + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"> |
| 430 | - <span class="result-sigla">({item.sigla})</span> | 624 | + <polyline points="20 6 9 17 4 12"/> |
| 625 | + </svg> | ||
| 431 | {/if} | 626 | {/if} |
| 432 | - </div> | 627 | + </span> |
| 433 | - <div class="result-meta"> | 628 | + <span class="checkbox-label">{clf.label}</span> |
| 434 | - {#if item.codigo} | 629 | + {#if clf.disabled} |
| 435 | - <span class="result-code">{item.codigo}</span> | 630 | + <span class="chip-soon">próx.</span> |
| 436 | {/if} | 631 | {/if} |
| 437 | - <span class="result-type" style="color:{getTypeColor(item.tipo)}">{getTypeLabel(item.tipo)}</span> | ||
| 438 | - </div> | ||
| 439 | </button> | 632 | </button> |
| 440 | {/each} | 633 | {/each} |
| 441 | </div> | 634 | </div> |
| 442 | - {:else if searchFocused && $query.length >= 2 && filteredResults.length === 0} | ||
| 443 | - <div class="search-results"> | ||
| 444 | - <div class="search-no-results">Sin resultados para "{$query}"</div> | ||
| 445 | </div> | 635 | </div> |
| 446 | {/if} | 636 | {/if} |
| 637 | + </div> | ||
| 447 | 638 | ||
| 448 | - {#if $error} | 639 | + <!-- ─── ÁREA DE RESULTADOS / EJEMPLOS / HINT ─── --> |
| 449 | - <div class="search-error">{$error}</div> | 640 | + <div class="results-area"> |
| 450 | - {/if} | ||
| 451 | 641 | ||
| 452 | - <!-- Mobile settings panel --> | 642 | + <!-- Ejemplos --> |
| 453 | - <div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}> | 643 | + <div class="examples" class:examples-hidden={isSearching}> |
| 454 | - <div class="mobile-meta-bar"> | 644 | + <span class="examples-label">Ej.:</span> |
| 455 | - <a href="/vista-diaria" class="mobile-meta-item mobile-diario-link"> | 645 | + {#each currentExamples as ex} |
| 456 | - Ver hoy | 646 | + <button |
| 457 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | 647 | + class="example-chip" |
| 458 | - <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | 648 | + style="background:{ex.color}; color:{ex.textColor}" |
| 649 | + on:click={() => searchExample(ex.label)} | ||
| 650 | + > | ||
| 651 | + {ex.label} | ||
| 652 | + </button> | ||
| 653 | + {/each} | ||
| 654 | + </div> | ||
| 655 | + | ||
| 656 | + <!-- Hint diario --> | ||
| 657 | + {#if !isSearching && !dailyLoading} | ||
| 658 | + <a | ||
| 659 | + href="/vista-diaria" | ||
| 660 | + class="daily-hint" | ||
| 661 | + class:daily-hint-visible={!isSearching} | ||
| 662 | + > | ||
| 663 | + <span class="daily-dot"></span> | ||
| 664 | + <span class="daily-text"> | ||
| 665 | + {#if dailyData} | ||
| 666 | + Ayer el Estado ejecutó <strong>{formatMonto(dailyData.monto)}</strong> | ||
| 667 | + {:else} | ||
| 668 | + Ver el resumen de ejecución de ayer | ||
| 669 | + {/if} | ||
| 670 | + </span> | ||
| 671 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 672 | + <path d="M5 12h14M12 5l7 7-7 7"/> | ||
| 459 | </svg> | 673 | </svg> |
| 460 | </a> | 674 | </a> |
| 461 | - <span class="mobile-meta-item mobile-search-badge"> | 675 | + {/if} |
| 462 | - <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | 676 | + |
| 463 | - <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | 677 | + <!-- Estados de búsqueda (mensajes) --> |
| 464 | - </svg> | 678 | + {#if showMinLengthHint && searchVal.length > 0 && searchVal.length < 3} |
| 465 | - Viendo 2005 – 2025 | 679 | + <div class="results-message"> |
| 680 | + <div class="results-hint">Escribe al menos 3 letras para buscar</div> | ||
| 681 | + </div> | ||
| 682 | + {:else if error && !results} | ||
| 683 | + <div class="results-message"> | ||
| 684 | + <div class="results-error">{error}</div> | ||
| 685 | + </div> | ||
| 686 | + {:else if isLoading && !results} | ||
| 687 | + <div class="results-message"> | ||
| 688 | + <div class="results-loading">Buscando...</div> | ||
| 689 | + </div> | ||
| 690 | + {:else if results && results.hits?.length === 0} | ||
| 691 | + <div class="results-message"> | ||
| 692 | + <div class="results-empty">Sin resultados para "{searchVal}"</div> | ||
| 693 | + </div> | ||
| 694 | + {/if} | ||
| 695 | + | ||
| 696 | + <!-- Contenedor de resultados (siempre montado para evitar flicker) --> | ||
| 697 | + <div | ||
| 698 | + class="results-content results-content-filled" | ||
| 699 | + class:results-content-visible={allHits.length > 0} | ||
| 700 | + > | ||
| 701 | + {#if stats} | ||
| 702 | + <div class="results-summary"> | ||
| 703 | + <div class="summary-row"> | ||
| 704 | + <div class="summary-left"> | ||
| 705 | + <div class="summary-header"> | ||
| 706 | + <span class="summary-count">{sortedHits.length}</span> | ||
| 707 | + <span class="summary-header-text">de {stats.found.toLocaleString()} resultados</span> | ||
| 708 | + {#if isLoading}<span class="updating-dot"></span>{/if} | ||
| 709 | + </div> | ||
| 710 | + <div class="summary-meta"> | ||
| 711 | + <span class="summary-tag summary-tag-gold"> | ||
| 712 | + {formatMonto(stats.montoTotal)} | ||
| 466 | </span> | 713 | </span> |
| 714 | + {#if stats.isClassifier} | ||
| 715 | + {#if stats.isEntidad && groupedHits} | ||
| 716 | + <span class="summary-tag summary-tag-entities">{stats.numEntidades} {stats.numEntidades === 1 ? 'entidad' : 'entidades'}</span> | ||
| 717 | + <span class="summary-tag">{groupedHits.length} {groupedHits.length === 1 ? 'categoría' : 'categorías'}</span> | ||
| 718 | + {/if} | ||
| 719 | + {:else} | ||
| 720 | + <span class="summary-tag">en {stats.numEntidades} {stats.numEntidades === 1 ? 'entidad' : 'entidades'}</span> | ||
| 721 | + {#if stats.yearRange} | ||
| 722 | + <span class="summary-tag">{stats.yearRange}</span> | ||
| 723 | + {/if} | ||
| 724 | + {/if} | ||
| 467 | </div> | 725 | </div> |
| 468 | - <div class="mobile-filter-section"> | 726 | + </div> |
| 469 | - <div class="mobile-filter-title">Buscar</div> | 727 | + <div class="sort-buttons"> |
| 470 | - <div class="mobile-filter-bar"> | 728 | + {#if !stats.isClassifier} |
| 471 | - <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.entidad} on:click={() => toggleFilter('entidad')}> | 729 | + <button class="sort-btn" class:sort-btn-active={sortBy === 'year'} on:click={() => toggleSort('year')}> |
| 472 | - <span class="filter-chip-dot" style="background:#4A8B6E"></span> | 730 | + Año |
| 473 | - Instituciones | 731 | + {#if sortBy === 'year'} |
| 474 | - </button> | 732 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| 475 | - <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.objeto_gasto} on:click={() => toggleFilter('objeto_gasto')}> | 733 | + {#if sortOrder === 'desc'}<path d="M12 5v14M5 12l7 7 7-7"/> |
| 476 | - <span class="filter-chip-dot" style="background:#C9A751"></span> | 734 | + {:else}<path d="M12 19V5M5 12l7-7 7 7"/>{/if} |
| 477 | - Gastos | 735 | + </svg> |
| 736 | + {/if} | ||
| 478 | </button> | 737 | </button> |
| 479 | - <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.rubro} on:click={() => toggleFilter('rubro')}> | 738 | + {/if} |
| 480 | - <span class="filter-chip-dot" style="background:#8B6E9B"></span> | 739 | + <button class="sort-btn" class:sort-btn-active={sortBy === 'amount'} on:click={() => toggleSort('amount')}> |
| 481 | - Ingresos | 740 | + Monto |
| 741 | + {#if sortBy === 'amount'} | ||
| 742 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 743 | + {#if sortOrder === 'desc'}<path d="M12 5v14M5 12l7 7 7-7"/> | ||
| 744 | + {:else}<path d="M12 19V5M5 12l7-7 7 7"/>{/if} | ||
| 745 | + </svg> | ||
| 746 | + {/if} | ||
| 482 | </button> | 747 | </button> |
| 483 | </div> | 748 | </div> |
| 484 | </div> | 749 | </div> |
| 485 | </div> | 750 | </div> |
| 751 | + {/if} | ||
| 486 | 752 | ||
| 487 | - <!-- Pills --> | 753 | + <div class="results-list"> |
| 488 | - <div class="pills"> | 754 | + {#if groupedHits} |
| 489 | - <span class="pills-label">Ej:</span> | 755 | + <!-- Vista agrupada para entidades --> |
| 490 | - {#each examples as ex} | 756 | + {#each groupedHits as group, gi} |
| 491 | - {#if ex.href} | 757 | + <div class="result-group"> |
| 492 | - <a class="pill" style="color:{ex.color};background:{ex.bg}" href={ex.href}>{ex.label}</a> | 758 | + <div class="result-group-header"> |
| 759 | + <span class="result-group-name">{group.name}</span> | ||
| 760 | + <span class="result-group-count">{group.hits.length}</span> | ||
| 761 | + <span class="result-group-monto">{formatMonto(group.totalMonto)}</span> | ||
| 762 | + </div> | ||
| 763 | + <div class="result-group-items"> | ||
| 764 | + {#each group.hits as hit, i (getHitKey(hit, gi * 1000 + i))} | ||
| 765 | + {@const meta = parseMetadatos(hit.document.metadatos)} | ||
| 766 | + {@const isDA = meta.da && meta.entidad_desc_entidad} | ||
| 767 | + {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} | ||
| 768 | + <a href="/entidad/{meta.entidad}" class="result-card result-card-grouped"> | ||
| 769 | + <div class="result-content"> | ||
| 770 | + <div class="result-text"> | ||
| 771 | + {#if hit.highlights?.[0]?.snippet} | ||
| 772 | + {@html hit.highlights[0].snippet} | ||
| 773 | + {:else} | ||
| 774 | + {hit.document.texto} | ||
| 775 | + {/if} | ||
| 776 | + </div> | ||
| 777 | + <div class="result-meta"> | ||
| 778 | + {#if isDA}<span class="result-parent">Dependiente de {parentEntity}</span>{/if} | ||
| 779 | + <span class="result-monto">{formatMonto(hit.document.devengado)}</span> | ||
| 780 | + </div> | ||
| 781 | + </div> | ||
| 782 | + </a> | ||
| 783 | + {/each} | ||
| 784 | + </div> | ||
| 785 | + </div> | ||
| 786 | + {/each} | ||
| 787 | + {:else} | ||
| 788 | + <!-- Vista plana para otros tipos --> | ||
| 789 | + {#each sortedHits as hit, i (getHitKey(hit, i))} | ||
| 790 | + {@const meta = parseMetadatos(hit.document.metadatos)} | ||
| 791 | + {@const isEntidadClass = hit.document.class_ === 'entidad'} | ||
| 792 | + {@const isDA = isEntidadClass && meta.da && meta.entidad_desc_entidad} | ||
| 793 | + {@const subarea = isEntidadClass ? extractAfterHyphen(meta.entidad_desc_subarea) : null} | ||
| 794 | + {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} | ||
| 795 | + {@const isClassResult = hit.document.is_class === true} | ||
| 796 | + <a | ||
| 797 | + href={isEntidadClass | ||
| 798 | + ? `/entidad/${meta.entidad}` | ||
| 799 | + : (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)} | ||
| 800 | + class="result-card" | ||
| 801 | + > | ||
| 802 | + <div class="result-content"> | ||
| 803 | + <div class="result-text"> | ||
| 804 | + {#if hit.highlights?.[0]?.snippet} | ||
| 805 | + {@html hit.highlights[0].snippet} | ||
| 806 | + {:else} | ||
| 807 | + {hit.document.texto} | ||
| 808 | + {/if} | ||
| 809 | + </div> | ||
| 810 | + <div class="result-meta"> | ||
| 811 | + {#if isEntidadClass} | ||
| 812 | + {#if isDA}<span class="result-parent">Dependiente de {parentEntity}</span>{/if} | ||
| 813 | + {#if subarea}<span class="result-subarea">{subarea}</span>{/if} | ||
| 814 | + {:else if isClassResult} | ||
| 815 | + <span class="result-class-type">{hit.document.class_}</span> | ||
| 493 | {:else} | 816 | {:else} |
| 494 | - <button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button> | 817 | + <span class="result-year">{hit.document.gestion}</span> |
| 495 | {/if} | 818 | {/if} |
| 819 | + <span class="result-monto">{formatMonto(hit.document.devengado)}</span> | ||
| 820 | + </div> | ||
| 821 | + </div> | ||
| 822 | + <svg class="result-link-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 823 | + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/> | ||
| 824 | + <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/> | ||
| 825 | + </svg> | ||
| 826 | + </a> | ||
| 496 | {/each} | 827 | {/each} |
| 828 | + {/if} | ||
| 497 | </div> | 829 | </div> |
| 830 | + | ||
| 831 | + {#if hasMoreResults} | ||
| 832 | + <div class="load-more-container"> | ||
| 833 | + <button class="load-more-btn" on:click={loadMore} disabled={isLoadingMore}> | ||
| 834 | + {#if isLoadingMore} | ||
| 835 | + <span class="loading-dot"></span> Cargando... | ||
| 836 | + {:else} | ||
| 837 | + Cargar más ({allHits.length} de {results.found}) | ||
| 838 | + {/if} | ||
| 839 | + </button> | ||
| 840 | + </div> | ||
| 841 | + {/if} | ||
| 842 | + </div> | ||
| 843 | + | ||
| 844 | + <div class="area-spacer" class:area-spacer-hidden={allHits.length > 0}></div> | ||
| 498 | </div> | 845 | </div> |
| 499 | </div> | 846 | </div> |
| 500 | </div> | 847 | </div> |
| 501 | 848 | ||
| 502 | <!-- Scroll hint --> | 849 | <!-- Scroll hint --> |
| 503 | - <div class="scroll-hint anim" style="--d:700ms" class:scroll-hint-hidden={searchFocused && $results.length > 0}> | 850 | + <div class="scroll-hint anim" style="--d:800ms" class:scroll-hint-hidden={isSearching}> |
| 504 | - <svg width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="#B8B5AD" stroke-width="1.2" stroke-linecap="round"> | 851 | + <svg class="scroll-icon-mouse" width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"> |
| 505 | <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/> | 852 | <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/> |
| 506 | </svg> | 853 | </svg> |
| 507 | - <span>Explora el portal</span> | 854 | + <svg class="scroll-icon-touch" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> |
| 855 | + <path d="M6 9l6 6 6-6"/> | ||
| 856 | + </svg> | ||
| 857 | + <span>Explora la página</span> | ||
| 508 | </div> | 858 | </div> |
| 509 | - </section> | 859 | + |
| 510 | - </section> | 860 | + </main> |
| 511 | 861 | ||
| 512 | <!-- ═══ PORTAL DIRECTORY ═══ --> | 862 | <!-- ═══ PORTAL DIRECTORY ═══ --> |
| 513 | <section class="directory" data-section="datos"> | 863 | <section class="directory" data-section="datos"> |
| ... | @@ -905,7 +1255,7 @@ | ... | @@ -905,7 +1255,7 @@ |
| 905 | .section-dot-active:hover{transform:scale(1.2)} | 1255 | .section-dot-active:hover{transform:scale(1.2)} |
| 906 | .section-dot-label{position:absolute;right:20px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;letter-spacing:0.05em;color:#6B6860;white-space:nowrap;opacity:0;transition:opacity 0.2s;pointer-events:none} | 1256 | .section-dot-label{position:absolute;right:20px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;letter-spacing:0.05em;color:#6B6860;white-space:nowrap;opacity:0;transition:opacity 0.2s;pointer-events:none} |
| 907 | .section-dot:hover .section-dot-label{opacity:1} | 1257 | .section-dot:hover .section-dot-label{opacity:1} |
| 908 | - .gold{color:#C9A751} | 1258 | + .gold{color:#F0C14D} |
| 909 | .container{max-width:1060px;margin:0 auto} | 1259 | .container{max-width:1060px;margin:0 auto} |
| 910 | 1260 | ||
| 911 | /* Reveal */ | 1261 | /* Reveal */ |
| ... | @@ -932,6 +1282,7 @@ | ... | @@ -932,6 +1282,7 @@ |
| 932 | .nav-logo img{height:100%;width:auto;object-fit:contain} | 1282 | .nav-logo img{height:100%;width:auto;object-fit:contain} |
| 933 | 1283 | ||
| 934 | /* ── HERO ── */ | 1284 | /* ── HERO ── */ |
| 1285 | + main[data-section="hero"]{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow-x:hidden;overflow-y:visible;position:relative} | ||
| 935 | .landing-screen{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} | 1286 | .landing-screen{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} |
| 936 | .hero{background:#1C1C1A;color:#F5F0E8;flex:1;display:flex;flex-direction:column;justify-content:center;padding:40px 48px 32px;position:relative;overflow:hidden} | 1287 | .hero{background:#1C1C1A;color:#F5F0E8;flex:1;display:flex;flex-direction:column;justify-content:center;padding:40px 48px 32px;position:relative;overflow:hidden} |
| 937 | .dot-pattern{position:absolute;inset:0;opacity:0.02;background-image:radial-gradient(circle at 1px 1px,#B8B5AD 0.5px,transparent 0);background-size:40px 40px} | 1288 | .dot-pattern{position:absolute;inset:0;opacity:0.02;background-image:radial-gradient(circle at 1px 1px,#B8B5AD 0.5px,transparent 0);background-size:40px 40px} |
| ... | @@ -941,7 +1292,7 @@ | ... | @@ -941,7 +1292,7 @@ |
| 941 | .hero-search{display:flex;justify-content:center;position:relative;z-index:50} | 1292 | .hero-search{display:flex;justify-content:center;position:relative;z-index:50} |
| 942 | .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} | 1293 | .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} |
| 943 | .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} | 1294 | .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} |
| 944 | - .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#A5A29A;max-width:100%;margin-top:14px;line-height:1.6} | 1295 | + .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#D0CCC4;max-width:100%;margin-top:14px;line-height:1.6} |
| 945 | .hero-sub-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(165,162,154,0.5);transition:all 0.2s;padding-bottom:1px} | 1296 | .hero-sub-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(165,162,154,0.5);transition:all 0.2s;padding-bottom:1px} |
| 946 | .hero-sub-link:hover{color:#4A8B6E;border-bottom-color:#4A8B6E} | 1297 | .hero-sub-link:hover{color:#4A8B6E;border-bottom-color:#4A8B6E} |
| 947 | .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} | 1298 | .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} |
| ... | @@ -951,16 +1302,17 @@ | ... | @@ -951,16 +1302,17 @@ |
| 951 | .stat-link:hover{color:#C9A751;border-bottom-color:#C9A751} | 1302 | .stat-link:hover{color:#C9A751;border-bottom-color:#C9A751} |
| 952 | 1303 | ||
| 953 | /* Search */ | 1304 | /* Search */ |
| 954 | - .search-wrap{max-width:960px;width:100%;margin:0 auto;position:relative;z-index:9999} | 1305 | + .search-wrap{max-width:960px;width:100%;margin:0 auto;position:relative;z-index:9999;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} |
| 1306 | + .search-wrap-with-results{background:rgba(255,255,255,0.04);border-radius:20px;border:1px solid rgba(255,255,255,0.08);padding:6px} | ||
| 955 | .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} | 1307 | .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} |
| 956 | - .search-focused{background:rgba(255,255,255,0.07);box-shadow:0 0 0 1.5px rgba(201,167,81,0.35),0 8px 40px rgba(0,0,0,0.3)} | 1308 | + .search-focused{background:rgba(255,255,255,0.06);box-shadow:0 12px 40px rgba(0,0,0,0.4),0 4px 12px rgba(0,0,0,0.2)} |
| 957 | .search-has-results{border-radius:16px 16px 0 0} | 1309 | .search-has-results{border-radius:16px 16px 0 0} |
| 958 | .search-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:0 4px} | 1310 | .search-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:0 4px} |
| 959 | .search-meta-item{display:flex;align-items:center;gap:7px;font-family:var(--sans);font-size:13px;font-weight:500;padding:10px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} | 1311 | .search-meta-item{display:flex;align-items:center;gap:7px;font-family:var(--sans);font-size:13px;font-weight:500;padding:10px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} |
| 960 | .search-meta-item svg{display:block;flex-shrink:0} | 1312 | .search-meta-item svg{display:block;flex-shrink:0} |
| 961 | - .search-badge{color:#C9A751;background:rgba(201,167,81,0.12)} | 1313 | + .search-badge{color:#4A8B6E;background:rgba(74,139,110,0.12)} |
| 962 | - .search-diario-link{color:#4A8B6E;background:rgba(74,139,110,0.12)} | 1314 | + .search-diario-link{color:#C9A751;background:rgba(201,167,81,0.12)} |
| 963 | - .search-diario-link:hover{background:rgba(74,139,110,0.22);color:#5AA080} | 1315 | + .search-diario-link:hover{background:rgba(201,167,81,0.22);color:#D4B65E} |
| 964 | .search-diario-link svg{transition:transform 0.2s} | 1316 | .search-diario-link svg{transition:transform 0.2s} |
| 965 | .search-diario-link:hover svg{transform:translateX(3px)} | 1317 | .search-diario-link:hover svg{transform:translateX(3px)} |
| 966 | .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} | 1318 | .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} |
| ... | @@ -968,9 +1320,11 @@ | ... | @@ -968,9 +1320,11 @@ |
| 968 | .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} | 1320 | .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} |
| 969 | .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:#C9A751} | 1321 | .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:#C9A751} |
| 970 | .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none} | 1322 | .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none} |
| 971 | - .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1s step-end infinite;border-radius:1px;flex-shrink:0} | 1323 | + .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1s step-end infinite;border-radius:1px;flex-shrink:0;background:#C9A751} |
| 1324 | + .cursor-hidden{opacity:0} | ||
| 972 | .cursor-diario{background:#4A8B6E} | 1325 | .cursor-diario{background:#4A8B6E} |
| 973 | .cursor-archivo{background:#C9A751} | 1326 | .cursor-archivo{background:#C9A751} |
| 1327 | + .has-results{border-radius:14px;box-shadow:none;background:rgba(255,255,255,0.03)} | ||
| 974 | .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD} | 1328 | .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD} |
| 975 | .placeholder-mobile{display:none} | 1329 | .placeholder-mobile{display:none} |
| 976 | .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px} | 1330 | .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px} |
| ... | @@ -1003,9 +1357,9 @@ | ... | @@ -1003,9 +1357,9 @@ |
| 1003 | .mobile-meta-bar{display:flex;gap:10px;padding:4px 0} | 1357 | .mobile-meta-bar{display:flex;gap:10px;padding:4px 0} |
| 1004 | .mobile-meta-item{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;font-family:var(--sans);font-size:13px;font-weight:500;padding:12px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} | 1358 | .mobile-meta-item{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;font-family:var(--sans);font-size:13px;font-weight:500;padding:12px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} |
| 1005 | .mobile-meta-item svg{flex-shrink:0} | 1359 | .mobile-meta-item svg{flex-shrink:0} |
| 1006 | - .mobile-search-badge{color:#C9A751;background:rgba(201,167,81,0.12)} | 1360 | + .mobile-search-badge{color:#4A8B6E;background:rgba(74,139,110,0.12)} |
| 1007 | - .mobile-diario-link{color:#4A8B6E;background:rgba(74,139,110,0.12)} | 1361 | + .mobile-diario-link{color:#C9A751;background:rgba(201,167,81,0.12)} |
| 1008 | - .mobile-diario-link:hover,.mobile-diario-link:active{background:rgba(74,139,110,0.22)} | 1362 | + .mobile-diario-link:hover,.mobile-diario-link:active{background:rgba(201,167,81,0.22)} |
| 1009 | .mobile-filter-section{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.06)} | 1363 | .mobile-filter-section{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.06)} |
| 1010 | .mobile-filter-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px} | 1364 | .mobile-filter-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px} |
| 1011 | .mobile-filter-bar{display:flex;gap:8px;flex-wrap:wrap} | 1365 | .mobile-filter-bar{display:flex;gap:8px;flex-wrap:wrap} |
| ... | @@ -1041,9 +1395,138 @@ | ... | @@ -1041,9 +1395,138 @@ |
| 1041 | .pill:hover::before{opacity:1} | 1395 | .pill:hover::before{opacity:1} |
| 1042 | .pill:hover{transform:translateY(-1px)} | 1396 | .pill:hover{transform:translateY(-1px)} |
| 1043 | 1397 | ||
| 1044 | - .scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:1;transition:opacity 0.3s} | 1398 | + .scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:100;transition:opacity 0.3s;color:#B8B5AD} |
| 1045 | .scroll-hint-hidden{opacity:0;pointer-events:none} | 1399 | .scroll-hint-hidden{opacity:0;pointer-events:none} |
| 1046 | .scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7} | 1400 | .scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7} |
| 1401 | + .scroll-icon-mouse{display:block} | ||
| 1402 | + .scroll-icon-touch{display:none} | ||
| 1403 | + | ||
| 1404 | + /* ── NEW SEARCH STYLES ── */ | ||
| 1405 | + .search-container{width:100%;max-width:900px;margin:0 auto;padding:0 24px;display:flex;flex-direction:column;align-items:center;flex:1;justify-content:center} | ||
| 1406 | + .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;color:#F5F0E8;text-align:center;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);margin-bottom:4px} | ||
| 1407 | + .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#D0CCC4;text-align:center;line-height:1.6;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);margin-bottom:32px;margin-top:0} | ||
| 1408 | + .hero-collapsed{opacity:0;height:0;margin:0;overflow:hidden;pointer-events:none} | ||
| 1409 | + .cursor-hidden{opacity:0} | ||
| 1410 | + | ||
| 1411 | + /* Filters */ | ||
| 1412 | + .filters-row{display:flex;flex-direction:column;gap:16px;margin-top:20px;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1413 | + .filters-hidden{opacity:0;height:0;margin:0;overflow:hidden;pointer-events:none} | ||
| 1414 | + .filter-level{display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:center} | ||
| 1415 | + .filter-level-1{justify-content:center} | ||
| 1416 | + .filter-level-2{flex-direction:column;gap:8px} | ||
| 1417 | + .filter-level-label{font-family:var(--mono);font-size:11px;letter-spacing:0.08em;color:#9B9890} | ||
| 1418 | + | ||
| 1419 | + /* Segmented Control */ | ||
| 1420 | + .segmented-control{display:inline-flex;background:rgba(255,255,255,0.06);border-radius:10px;padding:4px;gap:2px} | ||
| 1421 | + .segmented-control-secondary{background:rgba(255,255,255,0.04)} | ||
| 1422 | + .segment{font-family:var(--sans);font-size:14px;font-weight:500;color:#8B8880;padding:10px 20px;border:none;background:transparent;border-radius:8px;cursor:pointer;transition:all 0.2s} | ||
| 1423 | + .segment:hover{color:#B8B5AD} | ||
| 1424 | + .segment-active{background:rgba(255,255,255,0.12);color:#F5F0E8} | ||
| 1425 | + .segment-secondary{font-size:13px;padding:8px 16px} | ||
| 1426 | + | ||
| 1427 | + /* Checkbox Chips */ | ||
| 1428 | + .checkbox-group{display:flex;flex-wrap:wrap;gap:8px;justify-content:center} | ||
| 1429 | + .checkbox-chip{display:flex;align-items:center;gap:8px;padding:8px 14px;border-radius:20px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);cursor:pointer;transition:all 0.2s;font-family:var(--sans);font-size:13px;color:#8B8880} | ||
| 1430 | + .checkbox-chip:hover{background:rgba(255,255,255,0.08);border-color:rgba(255,255,255,0.12)} | ||
| 1431 | + .checkbox-chip-active{background:rgba(74,139,110,0.15);border-color:rgba(74,139,110,0.4);color:#5AAF8A} | ||
| 1432 | + .checkbox-chip-disabled{opacity:0.4;cursor:not-allowed} | ||
| 1433 | + .checkbox-chip-disabled:hover{background:rgba(255,255,255,0.04);border-color:rgba(255,255,255,0.08)} | ||
| 1434 | + .checkbox-box{width:16px;height:16px;border-radius:4px;border:1.5px solid rgba(255,255,255,0.2);display:flex;align-items:center;justify-content:center;transition:all 0.2s;flex-shrink:0} | ||
| 1435 | + .checkbox-chip-active .checkbox-box{background:#4A8B6E;border-color:#4A8B6E} | ||
| 1436 | + .checkbox-label{line-height:1} | ||
| 1437 | + .chip-soon{font-family:var(--mono);font-size:9px;letter-spacing:0.05em;color:#6B6860;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px;margin-left:4px} | ||
| 1438 | + | ||
| 1439 | + /* Results Area */ | ||
| 1440 | + .results-area{width:100%;margin-top:24px;position:relative;min-height:60px;text-align:center} | ||
| 1441 | + .area-spacer{height:100px;transition:height 0.3s} | ||
| 1442 | + .area-spacer-hidden{height:0} | ||
| 1443 | + | ||
| 1444 | + /* Examples */ | ||
| 1445 | + .examples{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:center;transition:all 0.3s} | ||
| 1446 | + .examples-hidden{opacity:0;height:0;overflow:hidden;pointer-events:none} | ||
| 1447 | + .examples-label{font-family:var(--mono);font-size:11px;letter-spacing:0.08em;color:#9B9890} | ||
| 1448 | + .example-chip{font-family:var(--sans);font-size:13px;font-weight:500;padding:8px 14px;border-radius:20px;border:1px solid rgba(255,255,255,0.1);cursor:pointer;transition:all 0.2s} | ||
| 1449 | + .example-chip:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.3)} | ||
| 1450 | + | ||
| 1451 | + /* Daily Hint */ | ||
| 1452 | + .daily-hint{display:inline-flex;align-items:center;gap:8px;margin-top:28px;padding:8px 14px;background:transparent;border:1px solid rgba(74,139,110,0.25);border-radius:20px;transition:all 0.3s;opacity:0;transform:translateY(10px);text-decoration:none;font-size:13px} | ||
| 1453 | + .daily-hint-visible{opacity:1;transform:translateY(0)} | ||
| 1454 | + .daily-hint:hover{background:rgba(74,139,110,0.08);border-color:rgba(74,139,110,0.4)} | ||
| 1455 | + .daily-dot{width:6px;height:6px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite;flex-shrink:0} | ||
| 1456 | + .daily-text{font-family:var(--sans);font-size:12px;color:#9B9890} | ||
| 1457 | + .daily-text strong{color:#4A8B6E} | ||
| 1458 | + .daily-hint svg{color:#4A8B6E;opacity:0.6;transition:all 0.2s} | ||
| 1459 | + .daily-hint:hover svg{opacity:1;transform:translateX(3px)} | ||
| 1460 | + | ||
| 1461 | + /* Results Messages */ | ||
| 1462 | + .results-message{text-align:center;padding:20px} | ||
| 1463 | + .results-hint,.results-loading{font-family:var(--sans);font-size:14px;color:#6B6860} | ||
| 1464 | + .results-error{font-family:var(--sans);font-size:14px;color:#C44B3F} | ||
| 1465 | + .results-empty{font-family:var(--sans);font-size:14px;color:#8B8880} | ||
| 1466 | + | ||
| 1467 | + /* Results Content */ | ||
| 1468 | + .results-content{max-height:0;overflow:hidden;opacity:0;transition:all 0.4s cubic-bezier(0.16,1,0.3,1)} | ||
| 1469 | + .results-content-visible{max-height:60vh;overflow-y:auto;opacity:1;background:transparent;border-radius:0;border:none;padding:14px 10px;text-align:left} | ||
| 1470 | + | ||
| 1471 | + /* Results Summary */ | ||
| 1472 | + .results-summary{background:transparent;border-radius:0;padding:0 0 14px 0;margin-bottom:14px;border-bottom:1px solid rgba(255,255,255,0.08)} | ||
| 1473 | + .summary-row{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap} | ||
| 1474 | + .summary-left{display:flex;flex-direction:column;gap:8px} | ||
| 1475 | + .summary-header{display:flex;align-items:center;gap:8px} | ||
| 1476 | + .summary-count{font-family:var(--mono);font-size:24px;font-weight:600;color:#F0C14D} | ||
| 1477 | + .summary-header-text{font-family:var(--sans);font-size:14px;color:#A5A29A} | ||
| 1478 | + .updating-dot{width:6px;height:6px;border-radius:50%;background:#C9A751;animation:pulse 1s ease-in-out infinite} | ||
| 1479 | + .summary-meta{display:flex;gap:8px;flex-wrap:wrap} | ||
| 1480 | + .summary-tag{font-family:var(--mono);font-size:11px;letter-spacing:0.03em;padding:4px 10px;border-radius:6px;background:rgba(255,255,255,0.06);color:#8B8880} | ||
| 1481 | + .summary-tag-gold{background:rgba(240,193,77,0.18);color:#F0C14D} | ||
| 1482 | + .summary-tag-entities{background:rgba(74,139,110,0.12);color:#4A8B6E} | ||
| 1483 | + | ||
| 1484 | + /* Sort Buttons */ | ||
| 1485 | + .sort-buttons{display:flex;gap:8px} | ||
| 1486 | + .sort-btn{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:12px;color:#6B6860;padding:8px 12px;border-radius:8px;background:rgba(255,255,255,0.04);border:none;cursor:pointer;transition:all 0.2s} | ||
| 1487 | + .sort-btn:hover{background:rgba(255,255,255,0.08);color:#B8B5AD} | ||
| 1488 | + .sort-btn-active{background:rgba(201,167,81,0.12);color:#C9A751} | ||
| 1489 | + | ||
| 1490 | + /* Results List */ | ||
| 1491 | + .results-list{display:flex;flex-direction:column;gap:0} | ||
| 1492 | + | ||
| 1493 | + /* Result Group (for entidades) */ | ||
| 1494 | + .result-group{background:transparent;border-radius:0;overflow:hidden;border-bottom:1px solid rgba(255,255,255,0.06);padding-bottom:8px;margin-bottom:8px} | ||
| 1495 | + .result-group:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0} | ||
| 1496 | + .result-group-header{display:flex;align-items:center;gap:12px;padding:8px 0;background:transparent} | ||
| 1497 | + .result-group-name{font-family:var(--sans);font-size:13px;font-weight:500;color:#F5F0E8;flex:1} | ||
| 1498 | + .result-group-count{font-family:var(--mono);font-size:11px;color:#6B6860;background:rgba(255,255,255,0.06);padding:2px 8px;border-radius:4px} | ||
| 1499 | + .result-group-monto{font-family:var(--mono);font-size:12px;color:#F0C14D} | ||
| 1500 | + .result-group-items{display:flex;flex-direction:column} | ||
| 1501 | + | ||
| 1502 | + /* Result Card */ | ||
| 1503 | + .result-card{display:flex;align-items:center;gap:12px;padding:12px 14px;background:transparent;border-radius:8px;text-decoration:none;transition:all 0.15s;border-bottom:1px solid rgba(255,255,255,0.06)} | ||
| 1504 | + .result-card:last-child{border-bottom:none} | ||
| 1505 | + .result-card:hover{background:rgba(255,255,255,0.06)} | ||
| 1506 | + .result-card-grouped{border-radius:6px;border-bottom:none;padding:10px 12px;margin-left:8px} | ||
| 1507 | + .result-card-grouped:hover{background:rgba(255,255,255,0.03)} | ||
| 1508 | + .result-content{flex:1;min-width:0} | ||
| 1509 | + .result-text{font-family:var(--sans);font-size:14px;color:#F5F0E8;line-height:1.4} | ||
| 1510 | + .result-text :global(mark){background:rgba(240,193,77,0.85);color:#1C1C1A;border-radius:3px;padding:1px 5px;font-weight:600} | ||
| 1511 | + .result-meta{display:flex;align-items:center;gap:10px;margin-top:6px;flex-wrap:wrap} | ||
| 1512 | + .result-parent,.result-subarea,.result-class-type{font-family:var(--mono);font-size:11px;color:#8B8880} | ||
| 1513 | + .result-year{font-family:var(--mono);font-size:12px;color:#8B8880;background:rgba(255,255,255,0.06);padding:2px 8px;border-radius:4px} | ||
| 1514 | + .result-monto{font-family:var(--mono);font-size:12px;color:#F0C14D;margin-left:auto} | ||
| 1515 | + .result-link-icon{color:#6B6860;opacity:0;transition:all 0.2s;flex-shrink:0} | ||
| 1516 | + .result-card:hover .result-link-icon{opacity:1} | ||
| 1517 | + | ||
| 1518 | + /* Load More */ | ||
| 1519 | + .load-more-container{display:flex;justify-content:center;margin-top:14px;padding-top:14px;border-top:1px solid rgba(255,255,255,0.06)} | ||
| 1520 | + .load-more-btn{font-family:var(--sans);font-size:14px;color:#8B8880;padding:12px 24px;border-radius:10px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);cursor:pointer;transition:all 0.2s;display:flex;align-items:center;gap:8px} | ||
| 1521 | + .load-more-btn:hover{background:rgba(255,255,255,0.08);color:#F5F0E8} | ||
| 1522 | + .load-more-btn:disabled{opacity:0.5;cursor:not-allowed} | ||
| 1523 | + .loading-dot{width:6px;height:6px;border-radius:50%;background:#C9A751;animation:pulse 1s ease-in-out infinite} | ||
| 1524 | + .search-loading{display:flex;align-items:center;justify-content:center} | ||
| 1525 | + | ||
| 1526 | + /* Nav elements in hero */ | ||
| 1527 | + main[data-section="hero"] .nav-logo{position:absolute;top:24px;left:24px} | ||
| 1528 | + .nav-menu-btn{position:absolute;top:24px;right:24px;background:rgba(255,255,255,0.06);border:none;border-radius:10px;width:44px;height:44px;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#B8B5AD;transition:all 0.2s} | ||
| 1529 | + .nav-menu-btn:hover{background:rgba(255,255,255,0.1);color:#F5F0E8} | ||
| 1047 | 1530 | ||
| 1048 | /* ── DIRECTORY ── */ | 1531 | /* ── DIRECTORY ── */ |
| 1049 | .directory{background:#f5f5f7;min-height:100vh;display:flex;align-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:48px} | 1532 | .directory{background:#f5f5f7;min-height:100vh;display:flex;align-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:48px} |
| ... | @@ -1222,7 +1705,7 @@ | ... | @@ -1222,7 +1705,7 @@ |
| 1222 | ══════════════════════════════════════════════════════════════════════════ */ | 1705 | ══════════════════════════════════════════════════════════════════════════ */ |
| 1223 | @media(max-width:768px){ | 1706 | @media(max-width:768px){ |
| 1224 | .page{scroll-snap-type:y proximity} | 1707 | .page{scroll-snap-type:y proximity} |
| 1225 | - .landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal} | 1708 | + main[data-section="hero"],.landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal} |
| 1226 | 1709 | ||
| 1227 | /* Section dots - bottom horizontal bar */ | 1710 | /* Section dots - bottom horizontal bar */ |
| 1228 | .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000} | 1711 | .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000} |
| ... | @@ -1263,6 +1746,36 @@ | ... | @@ -1263,6 +1746,36 @@ |
| 1263 | .pills{gap:6px} | 1746 | .pills{gap:6px} |
| 1264 | .pill{font-size:11px;padding:4px 10px} | 1747 | .pill{font-size:11px;padding:4px 10px} |
| 1265 | .scroll-hint{bottom:20px} | 1748 | .scroll-hint{bottom:20px} |
| 1749 | + .scroll-icon-mouse{display:none} | ||
| 1750 | + .scroll-icon-touch{display:block} | ||
| 1751 | + | ||
| 1752 | + /* New search responsive */ | ||
| 1753 | + .search-container{padding:0 16px} | ||
| 1754 | + .hero-title{font-size:clamp(32px,7vw,48px);margin-bottom:4px} | ||
| 1755 | + .hero-sub{font-size:14px;margin-bottom:24px} | ||
| 1756 | + .filters-row{gap:12px;margin-top:16px} | ||
| 1757 | + .filter-level{flex-direction:column;gap:8px} | ||
| 1758 | + .segmented-control{width:100%;justify-content:center} | ||
| 1759 | + .segment{flex:1;text-align:center;padding:10px 12px;font-size:13px} | ||
| 1760 | + .checkbox-group{justify-content:flex-start} | ||
| 1761 | + .checkbox-chip{font-size:12px;padding:6px 12px} | ||
| 1762 | + .examples{flex-direction:row;gap:8px} | ||
| 1763 | + .examples-label{display:none} | ||
| 1764 | + .example-chip{font-size:12px;padding:6px 12px} | ||
| 1765 | + .daily-hint{padding:10px 14px;gap:8px} | ||
| 1766 | + .daily-text{font-size:13px} | ||
| 1767 | + .results-summary{padding:12px 14px} | ||
| 1768 | + .summary-row{flex-direction:column;gap:12px} | ||
| 1769 | + .summary-count{font-size:20px} | ||
| 1770 | + .summary-meta{flex-wrap:wrap} | ||
| 1771 | + .summary-tag{font-size:10px;padding:3px 8px} | ||
| 1772 | + .sort-buttons{width:100%;justify-content:flex-end} | ||
| 1773 | + .sort-btn{font-size:11px;padding:6px 10px} | ||
| 1774 | + .result-card{padding:12px 14px} | ||
| 1775 | + .result-text{font-size:13px} | ||
| 1776 | + .result-monto{font-size:11px} | ||
| 1777 | + .result-year{font-size:11px;padding:2px 6px} | ||
| 1778 | + .load-more-btn{font-size:13px;padding:10px 20px} | ||
| 1266 | 1779 | ||
| 1267 | /* Directory */ | 1780 | /* Directory */ |
| 1268 | .directory{padding:48px 24px} | 1781 | .directory{padding:48px 24px} |
| ... | @@ -1351,6 +1864,34 @@ | ... | @@ -1351,6 +1864,34 @@ |
| 1351 | .pill{font-size:10px;padding:4px 8px} | 1864 | .pill{font-size:10px;padding:4px 8px} |
| 1352 | .scroll-hint{display:none} | 1865 | .scroll-hint{display:none} |
| 1353 | 1866 | ||
| 1867 | + /* New search 480px */ | ||
| 1868 | + .search-container{padding:0 12px} | ||
| 1869 | + .hero-title{font-size:clamp(28px,8vw,36px)} | ||
| 1870 | + .hero-sub{font-size:13px;margin-bottom:20px} | ||
| 1871 | + main[data-section="hero"] .nav-logo{top:16px;left:16px;height:60px} | ||
| 1872 | + .nav-menu-btn{top:16px;right:16px;width:40px;height:40px} | ||
| 1873 | + .segmented-control{padding:3px} | ||
| 1874 | + .segment{padding:8px 10px;font-size:12px} | ||
| 1875 | + .segment-secondary{font-size:11px;padding:6px 8px} | ||
| 1876 | + .filter-level-label{font-size:10px} | ||
| 1877 | + .checkbox-chip{font-size:11px;padding:5px 10px;gap:6px} | ||
| 1878 | + .checkbox-box{width:14px;height:14px} | ||
| 1879 | + .example-chip{font-size:11px;padding:5px 10px} | ||
| 1880 | + .daily-hint{padding:8px 12px;border-radius:10px} | ||
| 1881 | + .daily-dot{width:6px;height:6px} | ||
| 1882 | + .daily-text{font-size:12px} | ||
| 1883 | + .results-summary{padding:10px 12px;border-radius:10px} | ||
| 1884 | + .summary-count{font-size:18px} | ||
| 1885 | + .summary-header-text{font-size:12px} | ||
| 1886 | + .result-group-header{padding:10px 12px} | ||
| 1887 | + .result-group-name{font-size:12px} | ||
| 1888 | + .result-card{padding:10px 12px;border-radius:8px} | ||
| 1889 | + .result-text{font-size:12px} | ||
| 1890 | + .result-meta{gap:6px;margin-top:4px} | ||
| 1891 | + .result-parent,.result-subarea,.result-class-type{font-size:10px} | ||
| 1892 | + .result-monto{font-size:10px} | ||
| 1893 | + .load-more-btn{font-size:12px;padding:8px 16px;border-radius:8px} | ||
| 1894 | + | ||
| 1354 | /* Directory */ | 1895 | /* Directory */ |
| 1355 | .directory{padding:40px 16px} | 1896 | .directory{padding:40px 16px} |
| 1356 | .directory-inner{gap:24px} | 1897 | .directory-inner{gap:24px} | ... | ... |
src/routes/api/search/+server.js
0 → 100644
| 1 | +// Proxy para evitar CORS | ||
| 2 | +const API_BASE = 'http://34.66.240.236/api/proyecto/search'; | ||
| 3 | + | ||
| 4 | +export async function GET({ url }) { | ||
| 5 | + const q = url.searchParams.get('q') || ''; | ||
| 6 | + const is_class = url.searchParams.get('is_class') || 'false'; | ||
| 7 | + const mode = url.searchParams.get('mode') || 'gastos'; // 'gastos' | 'ingresos' | ||
| 8 | + const class_ = url.searchParams.get('class_') || ''; // comma-separated: entidad, objeto, finfun, acteco | ||
| 9 | + const page = url.searchParams.get('page') || '1'; // Paginación | ||
| 10 | + const per_page = url.searchParams.get('per_page') || ''; // Resultados por página | ||
| 11 | + | ||
| 12 | + // Build URL with all parameters | ||
| 13 | + const apiUrl = new URL(API_BASE); | ||
| 14 | + apiUrl.searchParams.set('q', q); | ||
| 15 | + apiUrl.searchParams.set('is_class', is_class); | ||
| 16 | + apiUrl.searchParams.set('page', page); | ||
| 17 | + if (per_page) apiUrl.searchParams.set('per_page', per_page); | ||
| 18 | + if (mode) apiUrl.searchParams.set('mode', mode); | ||
| 19 | + if (class_) apiUrl.searchParams.set('class_', class_); | ||
| 20 | + | ||
| 21 | + try { | ||
| 22 | + const response = await fetch(apiUrl.toString()); | ||
| 23 | + | ||
| 24 | + if (!response.ok) { | ||
| 25 | + return new Response(JSON.stringify({ error: `API error: ${response.status}` }), { | ||
| 26 | + status: response.status, | ||
| 27 | + headers: { 'Content-Type': 'application/json' } | ||
| 28 | + }); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + const data = await response.json(); | ||
| 32 | + | ||
| 33 | + return new Response(JSON.stringify(data), { | ||
| 34 | + headers: { 'Content-Type': 'application/json' } | ||
| 35 | + }); | ||
| 36 | + } catch (err) { | ||
| 37 | + console.error('Proxy error:', err); | ||
| 38 | + return new Response(JSON.stringify({ error: 'Failed to fetch from API' }), { | ||
| 39 | + status: 500, | ||
| 40 | + headers: { 'Content-Type': 'application/json' } | ||
| 41 | + }); | ||
| 42 | + } | ||
| 43 | +} |
| ... | @@ -87,16 +87,20 @@ | ... | @@ -87,16 +87,20 @@ |
| 87 | let treemapHeight = $state(500); | 87 | let treemapHeight = $state(500); |
| 88 | let treemapContainer = $state(null); | 88 | let treemapContainer = $state(null); |
| 89 | let hoveredNode = $state(null); | 89 | let hoveredNode = $state(null); |
| 90 | + let pinnedNode = $state(null); // Nodo fijado para mobile (primer tap) | ||
| 91 | + let tooltipHovered = $state(false); // Para mantener tooltip visible al pasar mouse sobre él | ||
| 92 | + let isMobile = $state(false); // Detectar si es dispositivo táctil | ||
| 90 | let showTreemapHelp = $state(false); | 93 | let showTreemapHelp = $state(false); |
| 91 | let mapaSidebarOpen = $state(false); // Para abrir sidebar en móvil | 94 | let mapaSidebarOpen = $state(false); // Para abrir sidebar en móvil |
| 92 | let mapaSidebarCollapsed = $state(false); // Para colapsar sidebar en desktop | 95 | let mapaSidebarCollapsed = $state(false); // Para colapsar sidebar en desktop |
| 93 | let sidebarYearDropdownOpen = $state(false); // Dropdown de año en sidebar | 96 | let sidebarYearDropdownOpen = $state(false); // Dropdown de año en sidebar |
| 94 | 97 | ||
| 95 | // Selector de nivel para vista aplanada | 98 | // Selector de nivel para vista aplanada |
| 96 | - let treemapViewLevel = $state('partida'); // 'jerarquico' | 'subgrupo' | 'partida' | 'subpartida' | 99 | + let treemapViewLevel = $state('partida'); // 'jerarquico' | 'grupo' | 'subgrupo' | 'partida' | 'subpartida' |
| 97 | let showConsolidado = $state(true); // true = consolidado (sin grupo 7), false = agregado (con transferencias) | 100 | let showConsolidado = $state(true); // true = consolidado (sin grupo 7), false = agregado (con transferencias) |
| 98 | const NIVEL_OPTIONS = [ | 101 | const NIVEL_OPTIONS = [ |
| 99 | { value: 'jerarquico', label: 'Jerarquía/Grupos', desc: 'Navegar por niveles' }, | 102 | { value: 'jerarquico', label: 'Jerarquía/Grupos', desc: 'Navegar por niveles' }, |
| 103 | + { value: 'grupo', label: 'Grupos', desc: '8-9 categorías' }, | ||
| 100 | { value: 'subgrupo', label: 'Subgrupos', desc: '~30 categorías' }, | 104 | { value: 'subgrupo', label: 'Subgrupos', desc: '~30 categorías' }, |
| 101 | { value: 'partida', label: 'Partidas', desc: '~100 categorías' }, | 105 | { value: 'partida', label: 'Partidas', desc: '~100 categorías' }, |
| 102 | { value: 'subpartida', label: 'Subpartidas', desc: '~300 categorías' } | 106 | { value: 'subpartida', label: 'Subpartidas', desc: '~300 categorías' } |
| ... | @@ -409,7 +413,7 @@ | ... | @@ -409,7 +413,7 @@ |
| 409 | const treemap = d3.treemap() | 413 | const treemap = d3.treemap() |
| 410 | .size([treemapWidth, treemapHeight]) | 414 | .size([treemapWidth, treemapHeight]) |
| 411 | .paddingOuter(4) | 415 | .paddingOuter(4) |
| 412 | - .paddingTop(22) // Espacio para el título del grupo | 416 | + .paddingTop(treemapViewLevel === 'grupo' ? 4 : 22) // Solo espacio extra para título si no estamos en nivel grupo |
| 413 | .paddingInner(2) | 417 | .paddingInner(2) |
| 414 | .round(true); | 418 | .round(true); |
| 415 | 419 | ||
| ... | @@ -432,8 +436,8 @@ | ... | @@ -432,8 +436,8 @@ |
| 432 | value: grupoNode.value | 436 | value: grupoNode.value |
| 433 | }); | 437 | }); |
| 434 | 438 | ||
| 435 | - // Agregar los items del grupo | 439 | + // Agregar los items del grupo (solo si no estamos en nivel grupo) |
| 436 | - if (grupoNode.children) { | 440 | + if (grupoNode.children && treemapViewLevel !== 'grupo') { |
| 437 | grupoNode.children.forEach(itemNode => { | 441 | grupoNode.children.forEach(itemNode => { |
| 438 | nodes.push({ | 442 | nodes.push({ |
| 439 | type: 'item', | 443 | type: 'item', |
| ... | @@ -543,6 +547,47 @@ | ... | @@ -543,6 +547,47 @@ |
| 543 | calculateTreemapLayout(); | 547 | calculateTreemapLayout(); |
| 544 | } | 548 | } |
| 545 | 549 | ||
| 550 | + // Navegar a vista de distribución por entidades | ||
| 551 | + function navigateToEntityDistribution(node) { | ||
| 552 | + // Obtener el código del objeto (puede ser grupo, subgrupo, partida o subpartida) | ||
| 553 | + const codigo = node.id || node.data?.objeto; | ||
| 554 | + if (codigo) { | ||
| 555 | + // Navegar a la ruta de distribución por entidades | ||
| 556 | + goto(`/objeto/${codigo}/entidades?gestion=${selectedYear}`); | ||
| 557 | + } | ||
| 558 | + } | ||
| 559 | + | ||
| 560 | + // Handler de click para nodos en vista plana (no jerárquica) | ||
| 561 | + function handleFlatNodeClick(node, e) { | ||
| 562 | + e.stopPropagation(); // Evitar que cierre el tooltip inmediatamente | ||
| 563 | + if (treemapViewLevel === 'jerarquico') return; // No aplica en modo jerárquico | ||
| 564 | + | ||
| 565 | + if (isMobile) { | ||
| 566 | + // Mobile: primer tap fija el tooltip, segundo tap (en botón) navega | ||
| 567 | + if (pinnedNode === node) { | ||
| 568 | + // Ya está fijado, no hacer nada (el botón maneja la navegación) | ||
| 569 | + return; | ||
| 570 | + } | ||
| 571 | + // Si hay otro nodo fijado, cambiar a este | ||
| 572 | + if (pinnedNode && pinnedNode !== node) { | ||
| 573 | + pinnedNode = node; | ||
| 574 | + hoveredNode = node; | ||
| 575 | + return; | ||
| 576 | + } | ||
| 577 | + // Fijar el tooltip en este nodo | ||
| 578 | + pinnedNode = node; | ||
| 579 | + hoveredNode = node; | ||
| 580 | + } else { | ||
| 581 | + // Desktop: click directo navega | ||
| 582 | + navigateToEntityDistribution(node); | ||
| 583 | + } | ||
| 584 | + } | ||
| 585 | + | ||
| 586 | + // Cerrar tooltip fijado | ||
| 587 | + function closePinnedTooltip() { | ||
| 588 | + pinnedNode = null; | ||
| 589 | + } | ||
| 590 | + | ||
| 546 | // Reactivo: recargar datos cuando cambia el año o nivel | 591 | // Reactivo: recargar datos cuando cambia el año o nivel |
| 547 | // Usar variable derivada para evitar loops en effects | 592 | // Usar variable derivada para evitar loops en effects |
| 548 | let treemapLoadTrigger = $derived(`${selectedYear}-${selectedEntity?.entidad ?? 0}-${treemapViewLevel}-${viewMode}-${showConsolidado}`); | 593 | let treemapLoadTrigger = $derived(`${selectedYear}-${selectedEntity?.entidad ?? 0}-${treemapViewLevel}-${viewMode}-${showConsolidado}`); |
| ... | @@ -720,6 +765,9 @@ | ... | @@ -720,6 +765,9 @@ |
| 720 | }); | 765 | }); |
| 721 | 766 | ||
| 722 | onMount(async () => { | 767 | onMount(async () => { |
| 768 | + // Detectar dispositivo táctil | ||
| 769 | + isMobile = 'ontouchstart' in window || navigator.maxTouchPoints > 0; | ||
| 770 | + | ||
| 723 | // Forzar recálculo de layout para navegación cliente | 771 | // Forzar recálculo de layout para navegación cliente |
| 724 | // Esto evita problemas de renderizado cuando se navega desde otra página | 772 | // Esto evita problemas de renderizado cuando se navega desde otra página |
| 725 | requestAnimationFrame(() => { | 773 | requestAnimationFrame(() => { |
| ... | @@ -2222,6 +2270,7 @@ | ... | @@ -2222,6 +2270,7 @@ |
| 2222 | width={treemapWidth} | 2270 | width={treemapWidth} |
| 2223 | height={treemapHeight} | 2271 | height={treemapHeight} |
| 2224 | class="block" | 2272 | class="block" |
| 2273 | + onclick={() => { if (pinnedNode) { pinnedNode = null; hoveredNode = null; } }} | ||
| 2225 | > | 2274 | > |
| 2226 | {#if treemapViewLevel === 'jerarquico'} | 2275 | {#if treemapViewLevel === 'jerarquico'} |
| 2227 | <!-- MODO JERÁRQUICO: con drill-down --> | 2276 | <!-- MODO JERÁRQUICO: con drill-down --> |
| ... | @@ -2314,7 +2363,56 @@ | ... | @@ -2314,7 +2363,56 @@ |
| 2314 | {@const textColorSecondary = getTextColorSecondary(grupoId)} | 2363 | {@const textColorSecondary = getTextColorSecondary(grupoId)} |
| 2315 | 2364 | ||
| 2316 | {#if node.type === 'grupo'} | 2365 | {#if node.type === 'grupo'} |
| 2317 | - <!-- Contenedor de grupo con título --> | 2366 | + {#if treemapViewLevel === 'grupo'} |
| 2367 | + <!-- Grupo como item principal (nivel grupo) --> | ||
| 2368 | + {@const pct = (node.value / flatTotalValue) * 100} | ||
| 2369 | + {@const area = width * height} | ||
| 2370 | + {@const scale = Math.sqrt(area) / 12} | ||
| 2371 | + {@const pctSize = Math.min(20, Math.max(9, scale * 1.5))} | ||
| 2372 | + {@const nameSize = Math.min(13, Math.max(8, scale * 0.95))} | ||
| 2373 | + {@const valueSize = Math.min(11, Math.max(7, scale * 0.75))} | ||
| 2374 | + <g | ||
| 2375 | + class="treemap-node" | ||
| 2376 | + transform="translate({node.x0}, {node.y0})" | ||
| 2377 | + onmouseenter={() => { if (!pinnedNode) hoveredNode = node; }} | ||
| 2378 | + onmouseleave={() => { if (!pinnedNode && !tooltipHovered) hoveredNode = null; }} | ||
| 2379 | + onclick={(e) => handleFlatNodeClick(node, e)} | ||
| 2380 | + style="cursor: pointer;" | ||
| 2381 | + > | ||
| 2382 | + <rect | ||
| 2383 | + width={width} | ||
| 2384 | + height={height} | ||
| 2385 | + fill={getNodeColor(grupoId, isHovered ? 1 : 0.85)} | ||
| 2386 | + stroke={isHovered ? 'var(--theme-titulo)' : 'transparent'} | ||
| 2387 | + stroke-width={isHovered ? 2 : 0} | ||
| 2388 | + rx="2" | ||
| 2389 | + /> | ||
| 2390 | + | ||
| 2391 | + {#if width > 35 && height > 22} | ||
| 2392 | + <foreignObject x="4" y="3" width={width - 8} height={height - 6}> | ||
| 2393 | + <div class="flex flex-col gap-0 overflow-hidden" style="font-family: var(--font-sans);"> | ||
| 2394 | + <div class="font-semibold leading-none opacity-75" style="font-size: {pctSize}px; color: {textColor};"> | ||
| 2395 | + {pct >= 10 ? pct.toFixed(0) : pct >= 1 ? pct.toFixed(1) : pct.toFixed(2)}% | ||
| 2396 | + </div> | ||
| 2397 | + {#if height > 40 && width > 50} | ||
| 2398 | + <div class="leading-tight mt-0.5 opacity-85" style="font-size: {nameSize}px; color: {textColor};"> | ||
| 2399 | + {width > 90 ? node.name : node.name.slice(0, Math.floor(width / 6)) + (node.name.length > Math.floor(width / 6) ? '…' : '')} | ||
| 2400 | + </div> | ||
| 2401 | + {/if} | ||
| 2402 | + {#if height > 55 && width > 55} | ||
| 2403 | + <div class="opacity-65" style="font-variant-numeric: tabular-nums; font-size: {valueSize}px; color: {textColor};"> | ||
| 2404 | + {formatMoneyCompact(node.value)} | ||
| 2405 | + </div> | ||
| 2406 | + <div class="opacity-65" style="font-variant-numeric: tabular-nums; font-size: {valueSize}px; color: {textColor};"> | ||
| 2407 | + {formatPerCapita(node.value)} | ||
| 2408 | + </div> | ||
| 2409 | + {/if} | ||
| 2410 | + </div> | ||
| 2411 | + </foreignObject> | ||
| 2412 | + {/if} | ||
| 2413 | + </g> | ||
| 2414 | + {:else} | ||
| 2415 | + <!-- Contenedor de grupo con título (niveles subgrupo, partida, subpartida) --> | ||
| 2318 | <g class="treemap-node" transform="translate({node.x0}, {node.y0})"> | 2416 | <g class="treemap-node" transform="translate({node.x0}, {node.y0})"> |
| 2319 | <!-- Fondo del grupo (sutil) --> | 2417 | <!-- Fondo del grupo (sutil) --> |
| 2320 | <rect | 2418 | <rect |
| ... | @@ -2348,6 +2446,7 @@ | ... | @@ -2348,6 +2446,7 @@ |
| 2348 | </text> | 2446 | </text> |
| 2349 | {/if} | 2447 | {/if} |
| 2350 | </g> | 2448 | </g> |
| 2449 | + {/if} | ||
| 2351 | {:else} | 2450 | {:else} |
| 2352 | <!-- Item individual --> | 2451 | <!-- Item individual --> |
| 2353 | {@const pct = (node.value / flatTotalValue) * 100} | 2452 | {@const pct = (node.value / flatTotalValue) * 100} |
| ... | @@ -2359,8 +2458,9 @@ | ... | @@ -2359,8 +2458,9 @@ |
| 2359 | <g | 2458 | <g |
| 2360 | class="treemap-node" | 2459 | class="treemap-node" |
| 2361 | transform="translate({node.x0}, {node.y0})" | 2460 | transform="translate({node.x0}, {node.y0})" |
| 2362 | - onmouseenter={() => hoveredNode = node} | 2461 | + onmouseenter={() => { if (!pinnedNode) hoveredNode = node; }} |
| 2363 | - onmouseleave={() => hoveredNode = null} | 2462 | + onmouseleave={() => { if (!pinnedNode && !tooltipHovered) hoveredNode = null; }} |
| 2463 | + onclick={(e) => handleFlatNodeClick(node, e)} | ||
| 2364 | style="cursor: pointer;" | 2464 | style="cursor: pointer;" |
| 2365 | > | 2465 | > |
| 2366 | <rect | 2466 | <rect |
| ... | @@ -2401,15 +2501,28 @@ | ... | @@ -2401,15 +2501,28 @@ |
| 2401 | </svg> | 2501 | </svg> |
| 2402 | 2502 | ||
| 2403 | <!-- Tooltip flotante (frosted glass) --> | 2503 | <!-- Tooltip flotante (frosted glass) --> |
| 2404 | - {#if hoveredNode && hoveredNode.type !== 'grupo'} | 2504 | + {#if hoveredNode && (hoveredNode.type !== 'grupo' || treemapViewLevel === 'grupo')} |
| 2405 | {@const totalValue = currentTreemapNode?.value || treemapRoot?.value || 1} | 2505 | {@const totalValue = currentTreemapNode?.value || treemapRoot?.value || 1} |
| 2506 | + {@const isPinned = pinnedNode === hoveredNode} | ||
| 2406 | <div | 2507 | <div |
| 2407 | class="treemap-tooltip" | 2508 | class="treemap-tooltip" |
| 2509 | + class:pinned={isPinned} | ||
| 2510 | + class:has-cta={treemapViewLevel !== 'jerarquico'} | ||
| 2408 | style=" | 2511 | style=" |
| 2409 | left: {Math.min(hoveredNode.x0 + 10, treemapWidth - 260)}px; | 2512 | left: {Math.min(hoveredNode.x0 + 10, treemapWidth - 260)}px; |
| 2410 | - top: {Math.min(hoveredNode.y0 + 10, treemapHeight - 100)}px; | 2513 | + top: {Math.min(hoveredNode.y0 + 10, treemapHeight - 140)}px; |
| 2411 | " | 2514 | " |
| 2515 | + onclick={(e) => e.stopPropagation()} | ||
| 2516 | + onmouseenter={() => { if (!isMobile && treemapViewLevel !== 'jerarquico') tooltipHovered = true; }} | ||
| 2517 | + onmouseleave={() => { tooltipHovered = false; hoveredNode = null; }} | ||
| 2412 | > | 2518 | > |
| 2519 | + {#if isPinned} | ||
| 2520 | + <button | ||
| 2521 | + class="tooltip-close" | ||
| 2522 | + onclick={() => { pinnedNode = null; hoveredNode = null; }} | ||
| 2523 | + aria-label="Cerrar" | ||
| 2524 | + >×</button> | ||
| 2525 | + {/if} | ||
| 2413 | {#if treemapViewLevel !== 'jerarquico'} | 2526 | {#if treemapViewLevel !== 'jerarquico'} |
| 2414 | <!-- Vista aplanada: mostrar contexto del grupo --> | 2527 | <!-- Vista aplanada: mostrar contexto del grupo --> |
| 2415 | <div class="font-medium text-sm">{hoveredNode.name || hoveredNode.id}</div> | 2528 | <div class="font-medium text-sm">{hoveredNode.name || hoveredNode.id}</div> |
| ... | @@ -2419,6 +2532,15 @@ | ... | @@ -2419,6 +2532,15 @@ |
| 2419 | <div class="text-xs opacity-60 mt-0.5" style="font-family: var(--font-sans); font-variant-numeric: tabular-nums;"> | 2532 | <div class="text-xs opacity-60 mt-0.5" style="font-family: var(--font-sans); font-variant-numeric: tabular-nums;"> |
| 2420 | {((hoveredNode.value / totalValue) * 100).toFixed(1)}% del gasto total | 2533 | {((hoveredNode.value / totalValue) * 100).toFixed(1)}% del gasto total |
| 2421 | </div> | 2534 | </div> |
| 2535 | + <!-- CTA para ver distribución por entidades --> | ||
| 2536 | + <div class="tooltip-cta"> | ||
| 2537 | + <button | ||
| 2538 | + class="tooltip-cta-btn" | ||
| 2539 | + onclick={() => navigateToEntityDistribution(hoveredNode)} | ||
| 2540 | + > | ||
| 2541 | + ¿Quién gasta más aquí? → | ||
| 2542 | + </button> | ||
| 2543 | + </div> | ||
| 2422 | {:else} | 2544 | {:else} |
| 2423 | <!-- Vista jerárquica --> | 2545 | <!-- Vista jerárquica --> |
| 2424 | <div class="font-medium text-sm">{hoveredNode.data?.desc_objeto || hoveredNode.name || hoveredNode.id}</div> | 2546 | <div class="font-medium text-sm">{hoveredNode.data?.desc_objeto || hoveredNode.name || hoveredNode.id}</div> |
| ... | @@ -4155,6 +4277,11 @@ | ... | @@ -4155,6 +4277,11 @@ |
| 4155 | font-family: var(--font-sans); | 4277 | font-family: var(--font-sans); |
| 4156 | } | 4278 | } |
| 4157 | 4279 | ||
| 4280 | + .treemap-tooltip.pinned, | ||
| 4281 | + .treemap-tooltip.has-cta { | ||
| 4282 | + pointer-events: auto; | ||
| 4283 | + } | ||
| 4284 | + | ||
| 4158 | :global(html.dark) .treemap-tooltip { | 4285 | :global(html.dark) .treemap-tooltip { |
| 4159 | background: rgba(30, 30, 30, 0.75); | 4286 | background: rgba(30, 30, 30, 0.75); |
| 4160 | border: 1px solid rgba(255, 255, 255, 0.1); | 4287 | border: 1px solid rgba(255, 255, 255, 0.1); |
| ... | @@ -4162,6 +4289,60 @@ | ... | @@ -4162,6 +4289,60 @@ |
| 4162 | color: #f5f5f5; | 4289 | color: #f5f5f5; |
| 4163 | } | 4290 | } |
| 4164 | 4291 | ||
| 4292 | + /* Tooltip close button (mobile) */ | ||
| 4293 | + .tooltip-close { | ||
| 4294 | + position: absolute; | ||
| 4295 | + top: 4px; | ||
| 4296 | + right: 6px; | ||
| 4297 | + width: 24px; | ||
| 4298 | + height: 24px; | ||
| 4299 | + background: transparent; | ||
| 4300 | + border: none; | ||
| 4301 | + color: inherit; | ||
| 4302 | + opacity: 0.5; | ||
| 4303 | + font-size: 18px; | ||
| 4304 | + line-height: 1; | ||
| 4305 | + cursor: pointer; | ||
| 4306 | + padding: 0; | ||
| 4307 | + display: flex; | ||
| 4308 | + align-items: center; | ||
| 4309 | + justify-content: center; | ||
| 4310 | + } | ||
| 4311 | + .tooltip-close:hover { | ||
| 4312 | + opacity: 1; | ||
| 4313 | + } | ||
| 4314 | + | ||
| 4315 | + /* Tooltip CTA */ | ||
| 4316 | + .tooltip-cta { | ||
| 4317 | + margin-top: 0.75rem; | ||
| 4318 | + padding-top: 0.625rem; | ||
| 4319 | + border-top: 1px solid rgba(0, 0, 0, 0.1); | ||
| 4320 | + } | ||
| 4321 | + :global(html.dark) .tooltip-cta { | ||
| 4322 | + border-top-color: rgba(255, 255, 255, 0.1); | ||
| 4323 | + } | ||
| 4324 | + | ||
| 4325 | + .tooltip-cta-btn { | ||
| 4326 | + width: 100%; | ||
| 4327 | + padding: 0.5rem 0.75rem; | ||
| 4328 | + background: var(--theme-accent); | ||
| 4329 | + color: white; | ||
| 4330 | + border: none; | ||
| 4331 | + border-radius: 6px; | ||
| 4332 | + font-size: 0.8125rem; | ||
| 4333 | + font-weight: 500; | ||
| 4334 | + cursor: pointer; | ||
| 4335 | + transition: background 0.15s ease, transform 0.1s ease; | ||
| 4336 | + text-align: left; | ||
| 4337 | + } | ||
| 4338 | + .tooltip-cta-btn:hover { | ||
| 4339 | + background: color-mix(in srgb, var(--theme-accent) 85%, black); | ||
| 4340 | + transform: translateX(2px); | ||
| 4341 | + } | ||
| 4342 | + .tooltip-cta-btn:active { | ||
| 4343 | + transform: scale(0.98); | ||
| 4344 | + } | ||
| 4345 | + | ||
| 4165 | /* Transiciones suaves para el treemap */ | 4346 | /* Transiciones suaves para el treemap */ |
| 4166 | :global(.treemap-node) { | 4347 | :global(.treemap-node) { |
| 4167 | transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; | 4348 | transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease; | ... | ... |
src/routes/nuevo-buscador/+page.svelte
0 → 100644
| 1 | +<script> | ||
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + import { goto } from '$app/navigation'; | ||
| 4 | + import { supabase } from '$lib/supabase'; | ||
| 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 | + // Pre-warm Supabase connection (evita cold start al navegar) | ||
| 20 | + async function warmupSupabase() { | ||
| 21 | + console.time('[WARMUP] Supabase'); | ||
| 22 | + await supabase.schema('ppto').from('vista_objeto_estado').select('objeto').limit(1); | ||
| 23 | + console.timeEnd('[WARMUP] Supabase'); | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + let mounted = false; | ||
| 27 | + let mode = 'archivo'; | ||
| 28 | + let searchFocused = false; | ||
| 29 | + let searchVal = ''; | ||
| 30 | + let hDiario = false; | ||
| 31 | + let hArchivo = false; | ||
| 32 | + let hVis = null; | ||
| 33 | + let hHist = null; | ||
| 34 | + let hWidget = null; | ||
| 35 | + let hGear = false; | ||
| 36 | + let mobileSettingsOpen = false; | ||
| 37 | + let gearMenuOpen = false; | ||
| 38 | + let searchInput; | ||
| 39 | + let debounceTimer; | ||
| 40 | + let autoFocused = false; | ||
| 41 | + | ||
| 42 | + // Search filters - which types to include | ||
| 43 | + let searchFilters = { | ||
| 44 | + entidad: true, | ||
| 45 | + objeto_gasto: true, | ||
| 46 | + rubro: true | ||
| 47 | + }; | ||
| 48 | + | ||
| 49 | + // Filtered results based on selected filters | ||
| 50 | + $: filteredResults = $results.filter(item => searchFilters[item.tipo]); | ||
| 51 | + | ||
| 52 | + function toggleFilter(tipo) { | ||
| 53 | + searchFilters[tipo] = !searchFilters[tipo]; | ||
| 54 | + searchFilters = searchFilters; // trigger reactivity | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + function toggleGearMenu() { | ||
| 58 | + gearMenuOpen = !gearMenuOpen; | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + onMount(() => { | ||
| 62 | + setTimeout(() => { mounted = true; }, 80); | ||
| 63 | + initIndex(); | ||
| 64 | + // Pre-calentar Supabase en background para evitar cold start | ||
| 65 | + warmupSupabase(); | ||
| 66 | + | ||
| 67 | + // Cerrar gear menu al hacer click fuera | ||
| 68 | + const handleClickOutside = (e) => { | ||
| 69 | + if (gearMenuOpen && !e.target.closest('.gear-wrap')) { | ||
| 70 | + gearMenuOpen = false; | ||
| 71 | + } | ||
| 72 | + }; | ||
| 73 | + document.addEventListener('click', handleClickOutside); | ||
| 74 | + return () => document.removeEventListener('click', handleClickOutside); | ||
| 75 | + }); | ||
| 76 | + | ||
| 77 | + // Auto-focus en el buscador cuando el índice esté listo (solo desktop) | ||
| 78 | + $: if ($indexLoaded && searchInput && !autoFocused) { | ||
| 79 | + autoFocused = true; | ||
| 80 | + // No auto-focus en móvil para evitar que aparezca el teclado | ||
| 81 | + const isMobile = typeof window !== 'undefined' && window.innerWidth < 768; | ||
| 82 | + if (!isMobile) { | ||
| 83 | + setTimeout(() => { | ||
| 84 | + searchInput?.focus(); | ||
| 85 | + }, 50); | ||
| 86 | + } | ||
| 87 | + } | ||
| 88 | + | ||
| 89 | + function toggleMobileSettings() { | ||
| 90 | + mobileSettingsOpen = !mobileSettingsOpen; | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + $: isD = mode === 'diario'; | ||
| 94 | + $: accent = isD ? '#4A8B6E' : '#C9A751'; | ||
| 95 | + $: placeholder = isD | ||
| 96 | + ? 'Busca una entidad, un ministerio, un municipio...' | ||
| 97 | + : 'Busca instituciones, gastos, ingresos, sectores...'; | ||
| 98 | + $: examples = isD | ||
| 99 | + ? [ | ||
| 100 | + { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, | ||
| 101 | + { label: 'Min. Educación', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, | ||
| 102 | + { label: 'GOB. La Paz', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, | ||
| 103 | + ] | ||
| 104 | + : [ | ||
| 105 | + { label: 'Sueldos', bg: 'rgba(201,167,81,0.12)', color: '#d4b962', href: '/objeto/11700' }, | ||
| 106 | + { label: 'Regalías', bg: 'rgba(160,150,170,0.09)', color: '#aaa0b0', href: '/rubro/14000' }, | ||
| 107 | + { label: 'BOA', bg: 'rgba(150,155,175,0.10)', color: '#a0a5b8' }, | ||
| 108 | + { label: 'MEFP', bg: 'rgba(140,170,150,0.10)', color: '#9ab8a8' }, | ||
| 109 | + { label: 'Publicidad', bg: 'rgba(180,130,100,0.10)', color: '#b8947a' }, | ||
| 110 | + ]; | ||
| 111 | + | ||
| 112 | + function switchMode(m) { | ||
| 113 | + mode = m; | ||
| 114 | + searchVal = ''; | ||
| 115 | + clearSearch(); | ||
| 116 | + if (searchInput) searchInput.value = ''; | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + function handleBlur() { | ||
| 120 | + setTimeout(() => { searchFocused = false; }, 200); | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + function handleInput(e) { | ||
| 124 | + const value = e.target.value; | ||
| 125 | + searchVal = value; | ||
| 126 | + clearTimeout(debounceTimer); | ||
| 127 | + debounceTimer = setTimeout(() => { | ||
| 128 | + performSearch(value); | ||
| 129 | + }, 250); | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + function handleKeydown(e) { | ||
| 133 | + if (!$showResults) return; | ||
| 134 | + switch (e.key) { | ||
| 135 | + case 'ArrowDown': | ||
| 136 | + e.preventDefault(); | ||
| 137 | + navigateResults('down', filteredResults.length); | ||
| 138 | + break; | ||
| 139 | + case 'ArrowUp': | ||
| 140 | + e.preventDefault(); | ||
| 141 | + navigateResults('up', filteredResults.length); | ||
| 142 | + break; | ||
| 143 | + case 'Enter': | ||
| 144 | + e.preventDefault(); | ||
| 145 | + // Si hay un resultado seleccionado, navegar a él | ||
| 146 | + if ($selectedIndex >= 0 && filteredResults[$selectedIndex]) { | ||
| 147 | + handleSelect(filteredResults[$selectedIndex]); | ||
| 148 | + } else if (filteredResults.length > 0) { | ||
| 149 | + // Si no hay selección pero hay resultados, ir al primero | ||
| 150 | + handleSelect(filteredResults[0]); | ||
| 151 | + } | ||
| 152 | + break; | ||
| 153 | + case 'Escape': | ||
| 154 | + clearSearch(); | ||
| 155 | + searchVal = ''; | ||
| 156 | + if (searchInput) searchInput.value = ''; | ||
| 157 | + searchInput?.blur(); | ||
| 158 | + break; | ||
| 159 | + } | ||
| 160 | + } | ||
| 161 | + | ||
| 162 | + function handleSelect(item) { | ||
| 163 | + if (item.tipo === 'entidad') { | ||
| 164 | + goto(`/entidad/${item.codigo}`); | ||
| 165 | + } else if (item.tipo === 'objeto_gasto') { | ||
| 166 | + goto(`/objeto/${item.codigo}`); | ||
| 167 | + } else if (item.tipo === 'rubro') { | ||
| 168 | + goto(`/rubro/${item.codigo}`); | ||
| 169 | + } | ||
| 170 | + clearSearch(); | ||
| 171 | + searchVal = ''; | ||
| 172 | + if (searchInput) searchInput.value = ''; | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + function getTypeLabel(tipo) { | ||
| 176 | + switch (tipo) { | ||
| 177 | + case 'entidad': return 'ENTIDAD'; | ||
| 178 | + case 'objeto_gasto': return 'OBJETO'; | ||
| 179 | + case 'rubro': return 'RUBRO'; | ||
| 180 | + default: return tipo?.toUpperCase() || ''; | ||
| 181 | + } | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + function getTypeColor(tipo) { | ||
| 185 | + switch (tipo) { | ||
| 186 | + case 'entidad': return '#4A8B6E'; | ||
| 187 | + case 'objeto_gasto': return '#C9A751'; | ||
| 188 | + case 'rubro': return '#8B6E9B'; | ||
| 189 | + default: return '#6B6860'; | ||
| 190 | + } | ||
| 191 | + } | ||
| 192 | + | ||
| 193 | + const WIDGETS_INGRESOS = [ | ||
| 194 | + { id: 'rubros', label: 'tipos de ingreso', d: 'Impuestos, regalías, ventas, créditos', t: 'Recursos por rubros', n: '324', href: '/clasificadores/rubros' }, | ||
| 195 | + { id: 'fuentes', label: 'fuentes de financiamiento', d: 'TGN, crédito externo, donaciones', t: 'Fuentes', n: '22', href: '/clasificadores/fuente' }, | ||
| 196 | + { id: 'organismos', label: 'organismos financiadores', d: 'BID, Banco Mundial, cooperación', t: 'Organismos', n: '228', href: '/clasificadores/organismos' }, | ||
| 197 | + ]; | ||
| 198 | + | ||
| 199 | + const WIDGETS_GASTOS_QUE = [ | ||
| 200 | + { id: 'objeto', label: 'objetos de gasto', d: 'Sueldos, consultorías, materiales', t: 'Objeto del gasto', n: '324', href: '/clasificadores/objeto-gasto' }, | ||
| 201 | + { id: 'sector', label: 'sectores económicos', d: 'Industria, construcción, servicios', t: 'Sectores', n: '488', href: '/clasificadores/sectores' }, | ||
| 202 | + ]; | ||
| 203 | + | ||
| 204 | + const WIDGETS_GASTOS_BOTTOM = [ | ||
| 205 | + { id: 'finalidad', label: 'finalidades y funciones', d: 'Educación, salud, defensa', t: 'Finalidad', n: '132', href: '/clasificadores/funcional' }, | ||
| 206 | + { id: 'donde', label: 'ubicaciones geográficas', d: 'Departamentos, municipios', t: 'Geográfico', n: '352', href: '/clasificadores/geografico' }, | ||
| 207 | + ]; | ||
| 208 | + | ||
| 209 | + const HISTORIAS = [ | ||
| 210 | + { color: '#C9A751', title: '¿Cuánto gasta Bolivia en consultorías cada año?', desc: 'Un análisis de 20 años revela patrones que todo ciudadano debería conocer. La evolución del gasto en servicios profesionales muestra tendencias inesperadas.', tag: 'INVESTIGACIÓN', tall: true }, | ||
| 211 | + { color: '#C44B3F', title: 'Salud vs. Defensa: 20 años', desc: 'Cómo se redistribuyó el presupuesto entre sectores.', tag: 'COMPARATIVA', tall: false }, | ||
| 212 | + { color: '#4A8B6E', title: 'El boom del gasto en infraestructura', desc: 'De 2006 a 2014 Bolivia multiplicó por 8 su inversión pública. Un recorrido por las grandes obras y su impacto fiscal.', tag: 'CRÓNICA', tall: true }, | ||
| 213 | + { color: '#6B8CA6', title: '¿Quién financia a Bolivia?', desc: 'Un mapa de los organismos internacionales que prestan al país.', tag: 'ANÁLISIS', tall: false }, | ||
| 214 | + ]; | ||
| 215 | + | ||
| 216 | + const VISUALIZACIONES = [ | ||
| 217 | + { color: '#4A8B6E', title: 'Evolución del gasto en salud', desc: 'Cómo cambiaron las prioridades en salud a lo largo de dos décadas.', type: 'Serie temporal', href: null }, | ||
| 218 | + { color: '#C9A751', title: '¿En qué se gasta?', desc: 'Explora el presupuesto por objeto de gasto.', type: 'Treemap', href: '/clasificadores/objeto-gasto?modo=mapa' }, | ||
| 219 | + { color: '#8B6E9B', title: 'Mapa de inversión por departamento', desc: 'Distribución geográfica de la inversión pública.', type: 'Mapa', href: null }, | ||
| 220 | + ]; | ||
| 221 | + | ||
| 222 | + /* Reveal action */ | ||
| 223 | + let revealStates = {}; | ||
| 224 | + function reveal(node, { id, delay = 0 }) { | ||
| 225 | + revealStates[id] = false; | ||
| 226 | + revealStates = revealStates; | ||
| 227 | + const obs = new IntersectionObserver(([e]) => { | ||
| 228 | + if (e.isIntersecting) { | ||
| 229 | + setTimeout(() => { revealStates[id] = true; revealStates = revealStates; }, delay); | ||
| 230 | + obs.disconnect(); | ||
| 231 | + } | ||
| 232 | + }, { threshold: 0.08 }); | ||
| 233 | + obs.observe(node); | ||
| 234 | + return { destroy() { obs.disconnect(); } }; | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + /* Section navigation */ | ||
| 238 | + const SECTIONS_NAV = [ | ||
| 239 | + { id: 'hero', label: 'Inicio' }, | ||
| 240 | + { id: 'datos', label: 'Datos' }, | ||
| 241 | + { id: 'clasificadores', label: 'Clasificadores' }, | ||
| 242 | + { id: 'historias', label: 'Historias' }, | ||
| 243 | + { id: 'visualizaciones', label: 'Visualizaciones' }, | ||
| 244 | + { id: 'manifiesto', label: 'Manifiesto' }, | ||
| 245 | + { id: 'descargas', label: 'Descargas' }, | ||
| 246 | + ]; | ||
| 247 | + let currentSection = 'hero'; | ||
| 248 | + | ||
| 249 | + onMount(() => { | ||
| 250 | + const sections = document.querySelectorAll('[data-section]'); | ||
| 251 | + const observer = new IntersectionObserver((entries) => { | ||
| 252 | + entries.forEach(entry => { | ||
| 253 | + if (entry.isIntersecting) { | ||
| 254 | + currentSection = entry.target.dataset.section; | ||
| 255 | + } | ||
| 256 | + }); | ||
| 257 | + }, { threshold: 0.5 }); | ||
| 258 | + sections.forEach(section => observer.observe(section)); | ||
| 259 | + return () => observer.disconnect(); | ||
| 260 | + }); | ||
| 261 | + | ||
| 262 | + function scrollToSection(id) { | ||
| 263 | + const el = document.querySelector(`[data-section="${id}"]`); | ||
| 264 | + if (el) el.scrollIntoView({ behavior: 'smooth' }); | ||
| 265 | + } | ||
| 266 | +</script> | ||
| 267 | + | ||
| 268 | +<div class="page" class:mounted> | ||
| 269 | + | ||
| 270 | + <!-- Section Navigation Dots --> | ||
| 271 | + <nav class="section-dots"> | ||
| 272 | + {#each SECTIONS_NAV as s} | ||
| 273 | + <button | ||
| 274 | + class="section-dot" | ||
| 275 | + class:section-dot-active={currentSection === s.id} | ||
| 276 | + on:click={() => scrollToSection(s.id)} | ||
| 277 | + aria-label={s.label} | ||
| 278 | + > | ||
| 279 | + <span class="section-dot-label">{s.label}</span> | ||
| 280 | + </button> | ||
| 281 | + {/each} | ||
| 282 | + </nav> | ||
| 283 | + | ||
| 284 | + <!-- ═══ LANDING SCREEN (NAV + HERO) ═══ --> | ||
| 285 | + <section class="landing-screen" data-section="hero"> | ||
| 286 | + <nav> | ||
| 287 | + <div class="nav-logo"><img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /></div> | ||
| 288 | + </nav> | ||
| 289 | + | ||
| 290 | + <!-- ═══ HERO ═══ --> | ||
| 291 | + <section class="hero"> | ||
| 292 | + <div class="dot-pattern"></div> | ||
| 293 | + <div class="hero-inner"> | ||
| 294 | + <!-- Bloque 1: Identidad --> | ||
| 295 | + <div class="hero-identity"> | ||
| 296 | + <h1 class="hero-title anim" style="--d:200ms">Presupuesto <span class="gold">Abierto</span></h1> | ||
| 297 | + <p class="hero-sub anim" style="--d:340ms"><a href="/vista-diaria" class="hero-sub-link">Información diaria</a> e histórica de todo el Estado Boliviano.</p> | ||
| 298 | + <div class="hero-stats anim" style="--d:420ms"> | ||
| 299 | + <span class="stat-num">21</span> años<span class="stat-dot">·</span><span class="stat-num">8.3M</span> registros<span class="stat-dot">·</span><span class="stat-num">647</span> instituciones<span class="stat-dot">·</span><span class="stat-num">7</span> <a href="#clasificadores" class="stat-link">clasificadores</a><span class="stat-dot">·</span><span class="stat-num">10</span> <a href="#visualizaciones" class="stat-link">visualizaciones</a><span class="stat-dot">·</span><span class="stat-num">4</span> <a href="#historias" class="stat-link">historias</a> | ||
| 300 | + </div> | ||
| 301 | + </div> | ||
| 302 | + | ||
| 303 | + <!-- Bloque 2: Búsqueda --> | ||
| 304 | + <div class="hero-search anim" style="--d:520ms"> | ||
| 305 | + <div class="search-wrap"> | ||
| 306 | + <!-- Search bar unificado --> | ||
| 307 | + <div class="search-bar" class:search-focused={searchFocused} class:search-has-results={searchFocused && $query.length >= 2 && ($results.length > 0 || $results.length === 0 || $isLoading)} style="--accent:{accent}"> | ||
| 308 | + <!-- Link al diario + Badge informativo (desktop) --> | ||
| 309 | + <div class="search-meta toggle-desktop"> | ||
| 310 | + <a href="/vista-diaria" class="search-meta-item search-diario-link"> | ||
| 311 | + Ver gasto de hoy | ||
| 312 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 313 | + <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 314 | + </svg> | ||
| 315 | + </a> | ||
| 316 | + <span class="search-meta-item search-badge"> | ||
| 317 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 318 | + <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | ||
| 319 | + </svg> | ||
| 320 | + Viendo 2005 – 2025 | ||
| 321 | + </span> | ||
| 322 | + </div> | ||
| 323 | + <div class="search-divider"></div> | ||
| 324 | + | ||
| 325 | + <!-- Mobile: mode indicator + settings button --> | ||
| 326 | + <button class="mobile-settings-btn" class:mobile-settings-open={mobileSettingsOpen} on:click={toggleMobileSettings}> | ||
| 327 | + <span class="mobile-mode-indicator" style="background:{accent}"></span> | ||
| 328 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={mobileSettingsOpen ? accent : '#B8B5AD'} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 329 | + <circle cx="12" cy="12" r="3"/> | ||
| 330 | + <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 331 | + </svg> | ||
| 332 | + </button> | ||
| 333 | + | ||
| 334 | + <svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={searchFocused ? accent : '#B8B5AD'} stroke-width="2" stroke-linecap="round"> | ||
| 335 | + <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> | ||
| 336 | + </svg> | ||
| 337 | + <div class="search-input-wrap"> | ||
| 338 | + <input | ||
| 339 | + type="text" | ||
| 340 | + bind:this={searchInput} | ||
| 341 | + bind:value={searchVal} | ||
| 342 | + on:input={handleInput} | ||
| 343 | + on:keydown={handleKeydown} | ||
| 344 | + on:focus={() => searchFocused = true} | ||
| 345 | + on:blur={handleBlur} | ||
| 346 | + disabled={!$indexLoaded} | ||
| 347 | + /> | ||
| 348 | + {#if !searchVal} | ||
| 349 | + <div class="search-placeholder"> | ||
| 350 | + <span class="blink-cursor" class:cursor-diario={isD} class:cursor-archivo={!isD}></span> | ||
| 351 | + <span class="placeholder-text placeholder-desktop">{placeholder}</span> | ||
| 352 | + <span class="placeholder-text placeholder-mobile">{isD ? 'MEFP, Min. Salud, GAM...' : 'BOA, Publicidad, IDH...'}</span> | ||
| 353 | + </div> | ||
| 354 | + {/if} | ||
| 355 | + {#if searchVal && !$isLoading} | ||
| 356 | + <button class="search-clear" on:click={() => { clearSearch(); searchVal = ''; if (searchInput) searchInput.value = ''; }}> | ||
| 357 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> | ||
| 358 | + </button> | ||
| 359 | + {/if} | ||
| 360 | + </div> | ||
| 361 | + <!-- Gear button with filter dropdown (desktop) --> | ||
| 362 | + <div class="gear-wrap gear-desktop" on:click|stopPropagation> | ||
| 363 | + <button class="gear-btn" class:gear-active={gearMenuOpen} on:click={toggleGearMenu}> | ||
| 364 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={gearMenuOpen ? accent : '#B8B5AD'} stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"> | ||
| 365 | + <circle cx="12" cy="12" r="3"/> | ||
| 366 | + <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/> | ||
| 367 | + </svg> | ||
| 368 | + </button> | ||
| 369 | + {#if gearMenuOpen} | ||
| 370 | + <div class="gear-menu"> | ||
| 371 | + <div class="gear-menu-title">Ajusta tu búsqueda</div> | ||
| 372 | + <button class="gear-row" on:click={() => toggleFilter('entidad')}> | ||
| 373 | + <div class="gear-check" class:gear-check-on={searchFilters.entidad}> | ||
| 374 | + {#if searchFilters.entidad} | ||
| 375 | + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 376 | + {/if} | ||
| 377 | + </div> | ||
| 378 | + <div class="gear-row-content"> | ||
| 379 | + <span class="gear-row-question">¿Quién?</span> | ||
| 380 | + <span class="gear-row-label" class:gear-row-label-on={searchFilters.entidad}>Instituciones</span> | ||
| 381 | + </div> | ||
| 382 | + <span class="gear-row-count">756</span> | ||
| 383 | + </button> | ||
| 384 | + <button class="gear-row" on:click={() => toggleFilter('objeto_gasto')}> | ||
| 385 | + <div class="gear-check" class:gear-check-on={searchFilters.objeto_gasto}> | ||
| 386 | + {#if searchFilters.objeto_gasto} | ||
| 387 | + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 388 | + {/if} | ||
| 389 | + </div> | ||
| 390 | + <div class="gear-row-content"> | ||
| 391 | + <span class="gear-row-question">¿En qué?</span> | ||
| 392 | + <span class="gear-row-label" class:gear-row-label-on={searchFilters.objeto_gasto}>Objetos de gasto</span> | ||
| 393 | + </div> | ||
| 394 | + <span class="gear-row-count">324</span> | ||
| 395 | + </button> | ||
| 396 | + <button class="gear-row" on:click={() => toggleFilter('rubro')}> | ||
| 397 | + <div class="gear-check" class:gear-check-on={searchFilters.rubro}> | ||
| 398 | + {#if searchFilters.rubro} | ||
| 399 | + <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> | ||
| 400 | + {/if} | ||
| 401 | + </div> | ||
| 402 | + <div class="gear-row-content"> | ||
| 403 | + <span class="gear-row-question">¿De dónde?</span> | ||
| 404 | + <span class="gear-row-label" class:gear-row-label-on={searchFilters.rubro}>Rubros de ingreso</span> | ||
| 405 | + </div> | ||
| 406 | + <span class="gear-row-count">324</span> | ||
| 407 | + </button> | ||
| 408 | + </div> | ||
| 409 | + {/if} | ||
| 410 | + </div> | ||
| 411 | + </div> | ||
| 412 | + | ||
| 413 | + <!-- Search Results Dropdown --> | ||
| 414 | + {#if $isLoading && $query.length >= 2} | ||
| 415 | + <div class="search-results"> | ||
| 416 | + <div class="search-loading">Buscando...</div> | ||
| 417 | + </div> | ||
| 418 | + {:else if searchFocused && filteredResults.length > 0} | ||
| 419 | + <div class="search-results"> | ||
| 420 | + {#each filteredResults as item, i} | ||
| 421 | + <button | ||
| 422 | + type="button" | ||
| 423 | + class="search-result-item" | ||
| 424 | + class:result-selected={$selectedIndex === i} | ||
| 425 | + on:click={() => handleSelect(item)} | ||
| 426 | + > | ||
| 427 | + <div class="result-main"> | ||
| 428 | + <span class="result-name">{item.nombre}</span> | ||
| 429 | + {#if item.sigla} | ||
| 430 | + <span class="result-sigla">({item.sigla})</span> | ||
| 431 | + {/if} | ||
| 432 | + </div> | ||
| 433 | + <div class="result-meta"> | ||
| 434 | + {#if item.codigo} | ||
| 435 | + <span class="result-code">{item.codigo}</span> | ||
| 436 | + {/if} | ||
| 437 | + <span class="result-type" style="color:{getTypeColor(item.tipo)}">{getTypeLabel(item.tipo)}</span> | ||
| 438 | + </div> | ||
| 439 | + </button> | ||
| 440 | + {/each} | ||
| 441 | + </div> | ||
| 442 | + {:else if searchFocused && $query.length >= 2 && filteredResults.length === 0} | ||
| 443 | + <div class="search-results"> | ||
| 444 | + <div class="search-no-results">Sin resultados para "{$query}"</div> | ||
| 445 | + </div> | ||
| 446 | + {/if} | ||
| 447 | + | ||
| 448 | + {#if $error} | ||
| 449 | + <div class="search-error">{$error}</div> | ||
| 450 | + {/if} | ||
| 451 | + | ||
| 452 | + <!-- Mobile settings panel --> | ||
| 453 | + <div class="mobile-settings-panel" class:mobile-settings-panel-open={mobileSettingsOpen}> | ||
| 454 | + <div class="mobile-meta-bar"> | ||
| 455 | + <a href="/vista-diaria" class="mobile-meta-item mobile-diario-link"> | ||
| 456 | + Ver hoy | ||
| 457 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 458 | + <line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/> | ||
| 459 | + </svg> | ||
| 460 | + </a> | ||
| 461 | + <span class="mobile-meta-item mobile-search-badge"> | ||
| 462 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 463 | + <rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/> | ||
| 464 | + </svg> | ||
| 465 | + Viendo 2005 – 2025 | ||
| 466 | + </span> | ||
| 467 | + </div> | ||
| 468 | + <div class="mobile-filter-section"> | ||
| 469 | + <div class="mobile-filter-title">Buscar</div> | ||
| 470 | + <div class="mobile-filter-bar"> | ||
| 471 | + <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.entidad} on:click={() => toggleFilter('entidad')}> | ||
| 472 | + <span class="filter-chip-dot" style="background:#4A8B6E"></span> | ||
| 473 | + Instituciones | ||
| 474 | + </button> | ||
| 475 | + <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.objeto_gasto} on:click={() => toggleFilter('objeto_gasto')}> | ||
| 476 | + <span class="filter-chip-dot" style="background:#C9A751"></span> | ||
| 477 | + Gastos | ||
| 478 | + </button> | ||
| 479 | + <button class="mobile-filter-chip" class:filter-chip-active={searchFilters.rubro} on:click={() => toggleFilter('rubro')}> | ||
| 480 | + <span class="filter-chip-dot" style="background:#8B6E9B"></span> | ||
| 481 | + Ingresos | ||
| 482 | + </button> | ||
| 483 | + </div> | ||
| 484 | + </div> | ||
| 485 | + </div> | ||
| 486 | + | ||
| 487 | + <!-- Pills --> | ||
| 488 | + <div class="pills"> | ||
| 489 | + <span class="pills-label">Ej:</span> | ||
| 490 | + {#each examples as ex} | ||
| 491 | + {#if ex.href} | ||
| 492 | + <a class="pill" style="color:{ex.color};background:{ex.bg}" href={ex.href}>{ex.label}</a> | ||
| 493 | + {:else} | ||
| 494 | + <button class="pill" style="color:{ex.color};background:{ex.bg}" on:click={() => searchVal = ex.label}>{ex.label}</button> | ||
| 495 | + {/if} | ||
| 496 | + {/each} | ||
| 497 | + </div> | ||
| 498 | + </div> | ||
| 499 | + </div> | ||
| 500 | + </div> | ||
| 501 | + | ||
| 502 | + <!-- Scroll hint --> | ||
| 503 | + <div class="scroll-hint anim" style="--d:700ms" class:scroll-hint-hidden={searchFocused && $results.length > 0}> | ||
| 504 | + <svg width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="#B8B5AD" stroke-width="1.2" stroke-linecap="round"> | ||
| 505 | + <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/> | ||
| 506 | + </svg> | ||
| 507 | + <span>Explora el portal</span> | ||
| 508 | + </div> | ||
| 509 | + </section> | ||
| 510 | + </section> | ||
| 511 | + | ||
| 512 | + <!-- ═══ PORTAL DIRECTORY ═══ --> | ||
| 513 | + <section class="directory" data-section="datos"> | ||
| 514 | + <div class="directory-inner"> | ||
| 515 | + <!-- Left: Title --> | ||
| 516 | + <div class="directory-left"> | ||
| 517 | + <div use:reveal={{ id: 'dir-head' }} class="reveal" class:revealed={revealStates['dir-head']}> | ||
| 518 | + <p class="section-tag">LOS DATOS</p> | ||
| 519 | + <h2 class="section-title">Dos formas<br/>de ver el<br/>presupuesto.</h2> | ||
| 520 | + </div> | ||
| 521 | + </div> | ||
| 522 | + | ||
| 523 | + <!-- Right: Cards --> | ||
| 524 | + <div class="directory-right"> | ||
| 525 | + <!-- Diario --> | ||
| 526 | + <div use:reveal={{ id: 'door-d', delay: 60 }} class="reveal" class:revealed={revealStates['door-d']}> | ||
| 527 | + <div class="door door-warm" class:door-hovered={hDiario} on:mouseenter={() => hDiario = true} on:mouseleave={() => hDiario = false} role="button" tabindex="0"> | ||
| 528 | + <div class="door-content"> | ||
| 529 | + <div class="door-header"> | ||
| 530 | + <div class="door-icon" style="opacity:{hDiario ? 1 : 0.4}"> | ||
| 531 | + <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#4A8B6E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 12 7 12 9 6 12 18 15 9 17 12 21 12"/></svg> | ||
| 532 | + </div> | ||
| 533 | + <div class="door-badge"> | ||
| 534 | + <div class="pulse-dot-sm"></div> | ||
| 535 | + <span class="door-badge-text" style="color:#4A8B6E">EN VIVO</span> | ||
| 536 | + </div> | ||
| 537 | + </div> | ||
| 538 | + <h3 class="door-title door-title-dark">Diariamente</h3> | ||
| 539 | + <div class="door-pills"> | ||
| 540 | + <span class="door-pill door-pill-green">¿Cuánto se ejecutó hoy?</span> | ||
| 541 | + <span class="door-pill door-pill-green">¿Quién gastó más?</span> | ||
| 542 | + <span class="door-pill door-pill-green">¿Qué cambió desde ayer?</span> | ||
| 543 | + <span class="door-pill door-pill-green">Panorama del presupuesto en curso</span> | ||
| 544 | + <span class="door-pill door-pill-green">Bitácora de movimientos</span> | ||
| 545 | + <span class="door-pill door-pill-green">Descarga de datos diaria</span> | ||
| 546 | + <span class="door-pill door-pill-green">Vista de todo el Estado</span> | ||
| 547 | + <span class="door-pill door-pill-green">Vista por Institución</span> | ||
| 548 | + </div> | ||
| 549 | + </div> | ||
| 550 | + <div class="door-footer"> | ||
| 551 | + <span class="door-meta">ACTUALIZADO HACE 2 HORAS</span> | ||
| 552 | + <span class="door-cta" style="color:{hDiario ? '#4A8B6E' : '#6B6860'}">Explorar →</span> | ||
| 553 | + </div> | ||
| 554 | + </div> | ||
| 555 | + </div> | ||
| 556 | + | ||
| 557 | + <!-- Archivo --> | ||
| 558 | + <div use:reveal={{ id: 'door-a', delay: 160 }} class="reveal" class:revealed={revealStates['door-a']}> | ||
| 559 | + <div class="door door-warm" class:door-hovered={hArchivo} on:mouseenter={() => hArchivo = true} on:mouseleave={() => hArchivo = false} role="button" tabindex="0"> | ||
| 560 | + <div class="door-content"> | ||
| 561 | + <div class="door-header"> | ||
| 562 | + <div class="door-icon" style="opacity:{hArchivo ? 1 : 0.4}"> | ||
| 563 | + <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#A68A3E" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="6" width="14" height="14" rx="2"/><path d="M7 6V4.5A1.5 1.5 0 0 1 8.5 3h7A1.5 1.5 0 0 1 17 4.5V6"/><line x1="8" y1="11" x2="14" y2="11"/><line x1="8" y1="14.5" x2="12" y2="14.5"/></svg> | ||
| 564 | + </div> | ||
| 565 | + <span class="door-badge-text" style="color:#A68A3E">2005 — 2025</span> | ||
| 566 | + </div> | ||
| 567 | + <h3 class="door-title door-title-dark">Históricamente</h3> | ||
| 568 | + <div class="door-pills"> | ||
| 569 | + <span class="door-pill door-pill-light">Tendencias</span> | ||
| 570 | + <span class="door-pill door-pill-light">Tipos de ingreso</span> | ||
| 571 | + <span class="door-pill door-pill-light">Tipos de gasto</span> | ||
| 572 | + <span class="door-pill door-pill-light">Por institución</span> | ||
| 573 | + <span class="door-pill door-pill-light">Por sector</span> | ||
| 574 | + <span class="door-pill door-pill-light">Por autonomías</span> | ||
| 575 | + <span class="door-pill door-pill-light">Comparar entidades</span> | ||
| 576 | + </div> | ||
| 577 | + </div> | ||
| 578 | + <div class="door-footer"> | ||
| 579 | + <span class="door-meta">8M REGISTROS · 7 CLASIFICADORES</span> | ||
| 580 | + <span class="door-cta" style="color:{hArchivo ? '#C9A751' : '#6B6860'}">Explorar →</span> | ||
| 581 | + </div> | ||
| 582 | + </div> | ||
| 583 | + </div> | ||
| 584 | + </div> | ||
| 585 | + </div> | ||
| 586 | + </section> | ||
| 587 | + | ||
| 588 | + <!-- ═══ CLASIFICADORES ═══ --> | ||
| 589 | + <section id="clasificadores" class="clasificadores" data-section="clasificadores"> | ||
| 590 | + <div class="clas-container"> | ||
| 591 | + <!-- Header --> | ||
| 592 | + <div use:reveal={{ id: 'clas-head' }} class="reveal clas-header-section" class:revealed={revealStates['clas-head']}> | ||
| 593 | + <div class="clas-header-row"> | ||
| 594 | + <div> | ||
| 595 | + <p class="section-tag">CLASIFICADORES</p> | ||
| 596 | + <h2 class="section-title">Elige cómo explorar el presupuesto</h2> | ||
| 597 | + <p class="clas-subtitle">Navega por cada una de las 2,626 piezas de los 8 clasificadores.</p> | ||
| 598 | + </div> | ||
| 599 | + <a href="/clasificadores" class="clas-ver-todos">Ver todos →</a> | ||
| 600 | + </div> | ||
| 601 | + </div> | ||
| 602 | + | ||
| 603 | + <!-- ¿Quién? --> | ||
| 604 | + <div use:reveal={{ id: 'clas-quien', delay: 100 }} class="reveal" class:revealed={revealStates['clas-quien']}> | ||
| 605 | + <div class="clas-group-card"> | ||
| 606 | + <div class="clas-group-header"> | ||
| 607 | + <div class="clas-group-icon" style="--c:#1C1C1A"> | ||
| 608 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg> | ||
| 609 | + </div> | ||
| 610 | + <div> | ||
| 611 | + <div class="clas-group-label">¿QUIÉN?</div> | ||
| 612 | + <div class="clas-group-sub">La entidad que gasta o recibe</div> | ||
| 613 | + </div> | ||
| 614 | + </div> | ||
| 615 | + <a href="/clasificadores/institucional" class="widget-card widget-h" class:widget-hover={hWidget === 'quien'} on:mouseenter={() => hWidget = 'quien'} on:mouseleave={() => hWidget = null}> | ||
| 616 | + <div class="widget-left"> | ||
| 617 | + <div class="widget-headline"> | ||
| 618 | + <span class="widget-num-big">756</span> | ||
| 619 | + <span class="widget-label">instituciones públicas</span> | ||
| 620 | + </div> | ||
| 621 | + <div class="widget-desc">Ministerios, gobernaciones, municipios, empresas públicas, universidades</div> | ||
| 622 | + </div> | ||
| 623 | + <div class="widget-right"> | ||
| 624 | + <span class="widget-tech">INSTITUCIONAL</span> | ||
| 625 | + <span class="widget-arrow" class:arrow-show={hWidget === 'quien'}>→</span> | ||
| 626 | + </div> | ||
| 627 | + </a> | ||
| 628 | + </div> | ||
| 629 | + </div> | ||
| 630 | + | ||
| 631 | + <!-- Grid: Ingresos + Gastos --> | ||
| 632 | + <div class="clas-main-grid"> | ||
| 633 | + <!-- INGRESOS --> | ||
| 634 | + <div use:reveal={{ id: 'clas-ing', delay: 200 }} class="reveal clas-col" class:revealed={revealStates['clas-ing']}> | ||
| 635 | + <div class="clas-group-card"> | ||
| 636 | + <div class="clas-group-header"> | ||
| 637 | + <div class="clas-group-icon" style="--c:#C9A751"> | ||
| 638 | + <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg> | ||
| 639 | + </div> | ||
| 640 | + <div> | ||
| 641 | + <div class="clas-group-label" style="color:#C9A751">INGRESOS</div> | ||
| 642 | + </div> | ||
| 643 | + </div> | ||
| 644 | + <div class="widgets-col"> | ||
| 645 | + {#each WIDGETS_INGRESOS as w} | ||
| 646 | + <a href={w.href} class="widget-card widget-grow" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}> | ||
| 647 | + <div class="widget-headline"> | ||
| 648 | + <span class="widget-num-big widget-num-gold">{w.n}</span> | ||
| 649 | + <span class="widget-label">{w.label}</span> | ||
| 650 | + </div> | ||
| 651 | + <p class="widget-desc">{w.d}</p> | ||
| 652 | + <div class="widget-footer"> | ||
| 653 | + <span class="widget-tech">{w.t}</span> | ||
| 654 | + <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span> | ||
| 655 | + </div> | ||
| 656 | + </a> | ||
| 657 | + {/each} | ||
| 658 | + </div> | ||
| 659 | + </div> | ||
| 660 | + </div> | ||
| 661 | + | ||
| 662 | + <!-- GASTOS --> | ||
| 663 | + <div use:reveal={{ id: 'clas-gas', delay: 300 }} class="reveal clas-col" class:revealed={revealStates['clas-gas']}> | ||
| 664 | + <div class="clas-group-card"> | ||
| 665 | + <div class="clas-group-header"> | ||
| 666 | + <div class="clas-group-icon" style="--c:#4A8B6E"> | ||
| 667 | + <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M12 5v14M5 12l7 7 7-7"/></svg> | ||
| 668 | + </div> | ||
| 669 | + <div> | ||
| 670 | + <div class="clas-group-label" style="color:#4A8B6E">GASTOS</div> | ||
| 671 | + </div> | ||
| 672 | + </div> | ||
| 673 | + | ||
| 674 | + <!-- Gastos content wrapper --> | ||
| 675 | + <div class="gastos-content"> | ||
| 676 | + <!-- ¿En qué? sub-group --> | ||
| 677 | + <div class="clas-subgroup"> | ||
| 678 | + <div class="clas-subgroup-header"> | ||
| 679 | + <div class="clas-subgroup-icon" style="--c:#4A8B6E"> | ||
| 680 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg> | ||
| 681 | + </div> | ||
| 682 | + <div> | ||
| 683 | + <div class="clas-subgroup-label">¿EN QUÉ?</div> | ||
| 684 | + </div> | ||
| 685 | + </div> | ||
| 686 | + <div class="widgets-row"> | ||
| 687 | + {#each WIDGETS_GASTOS_QUE as w} | ||
| 688 | + <a href={w.href} class="widget-card widget-sm" class:widget-hover={hWidget === w.id} on:mouseenter={() => hWidget = w.id} on:mouseleave={() => hWidget = null}> | ||
| 689 | + <div class="widget-headline"> | ||
| 690 | + <span class="widget-num-big widget-num-green">{w.n}</span> | ||
| 691 | + <span class="widget-label">{w.label}</span> | ||
| 692 | + </div> | ||
| 693 | + <p class="widget-desc widget-desc-sm">{w.d}</p> | ||
| 694 | + <div class="widget-footer"> | ||
| 695 | + <span class="widget-tech">{w.t}</span> | ||
| 696 | + <span class="widget-arrow" class:arrow-show={hWidget === w.id}>→</span> | ||
| 697 | + </div> | ||
| 698 | + </a> | ||
| 699 | + {/each} | ||
| 700 | + </div> | ||
| 701 | + </div> | ||
| 702 | + | ||
| 703 | + <!-- Bottom: Para qué + Dónde --> | ||
| 704 | + <div class="widgets-row"> | ||
| 705 | + <!-- ¿Para qué? --> | ||
| 706 | + <div class="clas-subgroup"> | ||
| 707 | + <div class="clas-subgroup-header"> | ||
| 708 | + <div class="clas-subgroup-icon" style="--c:#4A8B6E"> | ||
| 709 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></svg> | ||
| 710 | + </div> | ||
| 711 | + <div> | ||
| 712 | + <div class="clas-subgroup-label">¿PARA QUÉ?</div> | ||
| 713 | + </div> | ||
| 714 | + </div> | ||
| 715 | + <a href="/clasificadores/funcional" class="widget-card widget-full" class:widget-hover={hWidget === 'finalidad'} on:mouseenter={() => hWidget = 'finalidad'} on:mouseleave={() => hWidget = null}> | ||
| 716 | + <div class="widget-headline"> | ||
| 717 | + <span class="widget-num-big widget-num-green">132</span> | ||
| 718 | + <span class="widget-label">finalidades y funciones</span> | ||
| 719 | + </div> | ||
| 720 | + <p class="widget-desc">Educación, salud, defensa</p> | ||
| 721 | + <div class="widget-footer"> | ||
| 722 | + <span class="widget-tech">Finalidad</span> | ||
| 723 | + <span class="widget-arrow" class:arrow-show={hWidget === 'finalidad'}>→</span> | ||
| 724 | + </div> | ||
| 725 | + </a> | ||
| 726 | + </div> | ||
| 727 | + | ||
| 728 | + <!-- ¿Dónde? --> | ||
| 729 | + <div class="clas-subgroup"> | ||
| 730 | + <div class="clas-subgroup-header"> | ||
| 731 | + <div class="clas-subgroup-icon" style="--c:#4A8B6E"> | ||
| 732 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z"/><circle cx="12" cy="9" r="2.5"/></svg> | ||
| 733 | + </div> | ||
| 734 | + <div> | ||
| 735 | + <div class="clas-subgroup-label">¿DÓNDE?</div> | ||
| 736 | + </div> | ||
| 737 | + </div> | ||
| 738 | + <a href="/clasificadores/geografico" class="widget-card widget-full" class:widget-hover={hWidget === 'donde'} on:mouseenter={() => hWidget = 'donde'} on:mouseleave={() => hWidget = null}> | ||
| 739 | + <div class="widget-headline"> | ||
| 740 | + <span class="widget-num-big widget-num-green">352</span> | ||
| 741 | + <span class="widget-label">ubicaciones geográficas</span> | ||
| 742 | + </div> | ||
| 743 | + <p class="widget-desc">Departamentos, municipios</p> | ||
| 744 | + <div class="widget-footer"> | ||
| 745 | + <span class="widget-tech">Geográfico</span> | ||
| 746 | + <span class="widget-arrow" class:arrow-show={hWidget === 'donde'}>→</span> | ||
| 747 | + </div> | ||
| 748 | + </a> | ||
| 749 | + </div> | ||
| 750 | + </div> | ||
| 751 | + </div> | ||
| 752 | + </div> | ||
| 753 | + </div> | ||
| 754 | + </div> | ||
| 755 | + </div> | ||
| 756 | + </section> | ||
| 757 | + | ||
| 758 | + <!-- ═══ HISTORIAS ═══ --> | ||
| 759 | + <section id="historias" class="historias" data-section="historias"> | ||
| 760 | + <div class="container"> | ||
| 761 | + <div use:reveal={{ id: 'hist-head' }} class="reveal" class:revealed={revealStates['hist-head']}> | ||
| 762 | + <div class="hist-header"> | ||
| 763 | + <div> | ||
| 764 | + <div class="hist-tag-row"> | ||
| 765 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6860" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> | ||
| 766 | + <p class="section-tag">HISTORIAS</p> | ||
| 767 | + </div> | ||
| 768 | + <h2 class="section-title">Los datos, contados.</h2> | ||
| 769 | + <p class="hist-subtitle">Investigaciones y análisis narrativos sobre el presupuesto público.</p> | ||
| 770 | + </div> | ||
| 771 | + <a href="/historias" class="gold-link">Ver todas →</a> | ||
| 772 | + </div> | ||
| 773 | + </div> | ||
| 774 | + <div class="hist-grid"> | ||
| 775 | + {#each HISTORIAS as h, i} | ||
| 776 | + <div use:reveal={{ id: `hist-${i}`, delay: 60 + i * 80 }} class="reveal hist-cell" class:revealed={revealStates[`hist-${i}`]} class:hist-tall={h.tall}> | ||
| 777 | + <div class="hist-card" class:hist-card-hovered={hHist === i} on:mouseenter={() => hHist = i} on:mouseleave={() => hHist = null} role="button" tabindex="0" style="--card-color:{h.color}"> | ||
| 778 | + <div class="hist-accent" style="background:{h.color};transform:scaleX({hHist === i ? 1 : 0})"></div> | ||
| 779 | + <div class="hist-content"> | ||
| 780 | + <span class="hist-tag" style="color:{h.color}">{h.tag}</span> | ||
| 781 | + <h4 class="hist-title">{h.title}</h4> | ||
| 782 | + <p class="hist-desc">{h.desc}</p> | ||
| 783 | + </div> | ||
| 784 | + <div class="hist-cta" style="color:{hHist === i ? h.color : '#6B6860'}"> | ||
| 785 | + Leer historia <span class="hist-arrow" class:hist-arrow-moved={hHist === i}>→</span> | ||
| 786 | + </div> | ||
| 787 | + </div> | ||
| 788 | + </div> | ||
| 789 | + {/each} | ||
| 790 | + </div> | ||
| 791 | + </div> | ||
| 792 | + </section> | ||
| 793 | + | ||
| 794 | + <!-- ═══ VISUALIZACIONES ═══ --> | ||
| 795 | + <section id="visualizaciones" class="visualizaciones" data-section="visualizaciones"> | ||
| 796 | + <div class="container"> | ||
| 797 | + <div use:reveal={{ id: 'vis-head' }} class="reveal" class:revealed={revealStates['vis-head']}> | ||
| 798 | + <div class="vis-header"> | ||
| 799 | + <div> | ||
| 800 | + <div class="vis-tag-row"> | ||
| 801 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#6B6860" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg> | ||
| 802 | + <p class="section-tag">VISUALIZACIONES</p> | ||
| 803 | + </div> | ||
| 804 | + <h2 class="section-title">Explora los datos.</h2> | ||
| 805 | + <p class="vis-subtitle">Herramientas interactivas para navegar el presupuesto.</p> | ||
| 806 | + </div> | ||
| 807 | + <a href="/visualizaciones" class="gold-link">Ver todas →</a> | ||
| 808 | + </div> | ||
| 809 | + </div> | ||
| 810 | + <div class="vis-grid-simple"> | ||
| 811 | + {#each VISUALIZACIONES as v, i} | ||
| 812 | + <div use:reveal={{ id: `vis-${i}`, delay: 60 + i * 80 }} class="reveal" class:revealed={revealStates[`vis-${i}`]}> | ||
| 813 | + {#if v.href} | ||
| 814 | + <a href={v.href} class="vis-card vis-card-link" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null}> | ||
| 815 | + <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> | ||
| 816 | + <div> | ||
| 817 | + <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> | ||
| 818 | + <h4 class="vis-title">{v.title}</h4> | ||
| 819 | + <p class="vis-desc">{v.desc}</p> | ||
| 820 | + </div> | ||
| 821 | + <div class="vis-cta" style="color:{hVis === i ? v.color : '#6B6860'}"> | ||
| 822 | + Explorar <span class="vis-arrow" class:vis-arrow-moved={hVis === i}>→</span> | ||
| 823 | + </div> | ||
| 824 | + </a> | ||
| 825 | + {:else} | ||
| 826 | + <div class="vis-card vis-card-disabled" class:vis-card-hovered={hVis === i} on:mouseenter={() => hVis = i} on:mouseleave={() => hVis = null} role="button" tabindex="0"> | ||
| 827 | + <div class="vis-accent" style="background:{v.color};transform:scaleX({hVis === i ? 1 : 0})"></div> | ||
| 828 | + <div> | ||
| 829 | + <span class="vis-type" style="background:color-mix(in srgb, {v.color} 12%, transparent);color:{v.color}">{v.type}</span> | ||
| 830 | + <h4 class="vis-title">{v.title}</h4> | ||
| 831 | + <p class="vis-desc">{v.desc}</p> | ||
| 832 | + </div> | ||
| 833 | + <div class="vis-cta" style="color:#6B6860"> | ||
| 834 | + Próximamente | ||
| 835 | + </div> | ||
| 836 | + </div> | ||
| 837 | + {/if} | ||
| 838 | + </div> | ||
| 839 | + {/each} | ||
| 840 | + </div> | ||
| 841 | + </div> | ||
| 842 | + </section> | ||
| 843 | + | ||
| 844 | + <!-- ═══ MANIFIESTO ═══ --> | ||
| 845 | + <section class="manifiesto" data-section="manifiesto"> | ||
| 846 | + <div class="dot-pattern"></div> | ||
| 847 | + <div class="manifiesto-inner"> | ||
| 848 | + <div use:reveal={{ id: 'man' }} class="reveal" class:revealed={revealStates['man']}> | ||
| 849 | + <p class="section-tag-light">POR QUÉ EXISTE ESTO</p> | ||
| 850 | + <h2 class="manifiesto-title">El presupuesto público no es de los gobiernos.<br/><span class="gold">Es de todos.</span></h2> | ||
| 851 | + </div> | ||
| 852 | + </div> | ||
| 853 | + </section> | ||
| 854 | + | ||
| 855 | + <!-- ═══ DESCARGAS ═══ --> | ||
| 856 | + <section id="descargas" class="descargas" data-section="descargas"> | ||
| 857 | + <div class="descargas-inner"> | ||
| 858 | + <div use:reveal={{ id: 'desc' }} class="reveal" class:revealed={revealStates['desc']}> | ||
| 859 | + <h2 class="descargas-title">Descarga los datos.</h2> | ||
| 860 | + <div class="descargas-formats"> | ||
| 861 | + <span class="format-pill">.csv</span> | ||
| 862 | + <span class="format-pill">.parquet</span> | ||
| 863 | + <span class="format-pill">.gpkg</span> | ||
| 864 | + </div> | ||
| 865 | + <a href="/descargas" class="descargas-link"> | ||
| 866 | + <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v12"/><polyline points="8 12 12 16 16 12"/><path d="M4 18h16"/></svg> | ||
| 867 | + Ir a página de descargas | ||
| 868 | + </a> | ||
| 869 | + </div> | ||
| 870 | + </div> | ||
| 871 | + </section> | ||
| 872 | + | ||
| 873 | + <!-- ═══ FOOTER ═══ --> | ||
| 874 | + <footer> | ||
| 875 | + <div class="footer-inner"> | ||
| 876 | + <div class="footer-org">MINISTERIO DE ECONOMÍA Y FINANZAS PÚBLICAS</div> | ||
| 877 | + <div class="footer-right">DATOS ABIERTOS · 2005 — 2026</div> | ||
| 878 | + </div> | ||
| 879 | + </footer> | ||
| 880 | +</div> | ||
| 881 | + | ||
| 882 | +<style> | ||
| 883 | + @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap'); | ||
| 884 | + :global(*){margin:0;padding:0;box-sizing:border-box} | ||
| 885 | + :global(html){width:100%;height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A;scroll-behavior:smooth} | ||
| 886 | + :global(body){width:100%;min-height:100%;margin:0;padding:0;overflow-x:hidden;background:#1C1C1A} | ||
| 887 | + :global(::selection){background:#C9A751;color:#1C1C1A} | ||
| 888 | + :global(::-webkit-scrollbar){width:5px} | ||
| 889 | + :global(::-webkit-scrollbar-track){background:#1C1C1A} | ||
| 890 | + :global(::-webkit-scrollbar-thumb){background:#444;border-radius:3px} | ||
| 891 | + :global(a){color:inherit;text-decoration:none} | ||
| 892 | + | ||
| 893 | + @keyframes pulse{0%,100%{opacity:0.35}50%{opacity:1}} | ||
| 894 | + @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} | ||
| 895 | + @keyframes scrollHint{0%,100%{transform:translateY(0);opacity:0.6}50%{transform:translateY(5px);opacity:1}} | ||
| 896 | + @keyframes heartbeat{0%,100%{stroke-dashoffset:0}50%{stroke-dashoffset:48}} | ||
| 897 | + | ||
| 898 | + .page{--serif:'Qanelas',system-ui,sans-serif;--sans:'Qanelas',system-ui,sans-serif;--mono:'JetBrains Mono',monospace;font-family:var(--serif);color:#1C1C1A;width:100%;min-width:100%;overflow-x:hidden;scroll-snap-type:y mandatory;overflow-y:scroll;height:100vh} | ||
| 899 | + | ||
| 900 | + /* Section Navigation Dots */ | ||
| 901 | + .section-dots{position:fixed;right:24px;top:50%;transform:translateY(-50%);z-index:100;display:flex;flex-direction:column;gap:12px;background:rgba(255,255,255,0.1);backdrop-filter:blur(8px);padding:12px 8px;border-radius:20px} | ||
| 902 | + .section-dot{width:10px;height:10px;border-radius:50%;border:1.5px solid rgba(100,100,100,0.4);background:rgba(150,150,150,0.2);cursor:pointer;transition:all 0.3s;padding:0;position:relative} | ||
| 903 | + .section-dot:hover{border-color:rgba(100,100,100,0.6);background:rgba(150,150,150,0.4);transform:scale(1.2)} | ||
| 904 | + .section-dot-active{background:#C9A751;border-color:#C9A751} | ||
| 905 | + .section-dot-active:hover{transform:scale(1.2)} | ||
| 906 | + .section-dot-label{position:absolute;right:20px;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;letter-spacing:0.05em;color:#6B6860;white-space:nowrap;opacity:0;transition:opacity 0.2s;pointer-events:none} | ||
| 907 | + .section-dot:hover .section-dot-label{opacity:1} | ||
| 908 | + .gold{color:#C9A751} | ||
| 909 | + .container{max-width:1060px;margin:0 auto} | ||
| 910 | + | ||
| 911 | + /* Reveal */ | ||
| 912 | + .reveal{opacity:0;transform:translateY(28px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1)} | ||
| 913 | + .revealed{opacity:1!important;transform:translateY(0)!important} | ||
| 914 | + .anim{opacity:0;transform:translateY(18px);transition:all 0.8s cubic-bezier(0.16,1,0.3,1);transition-delay:var(--d,0ms)} | ||
| 915 | + .mounted .anim{opacity:1;transform:translateY(0)} | ||
| 916 | + | ||
| 917 | + /* ── NAV ── */ | ||
| 918 | + nav{background:#1C1C1A;padding:12px 48px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0} | ||
| 919 | + .nav-left{display:flex;align-items:center} | ||
| 920 | + .nav-right{display:flex;align-items:center;gap:24px} | ||
| 921 | + .nav-link{font-family:var(--sans);font-size:14px;color:#B8B5AD;letter-spacing:0.01em;font-weight:400;transition:color 0.2s} | ||
| 922 | + .nav-link:hover{color:#F5F0E8} | ||
| 923 | + .nav-link-icon{display:flex;align-items:center;gap:5px} | ||
| 924 | + .heartbeat-line{animation:heartbeat 3s ease-in-out infinite} | ||
| 925 | + .nav-date{display:flex;align-items:center;gap:6px;font-family:var(--mono);font-size:9px;letter-spacing:0.06em;color:#B8B5AD;font-weight:400} | ||
| 926 | + .date-label{color:#9B9890} | ||
| 927 | + .date-sep{margin:0 3px;opacity:0.3} | ||
| 928 | + .pulse-dot{width:5px;height:5px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite} | ||
| 929 | + .pulse-dot-sm{width:6px;height:6px;border-radius:50%;background:#4A8B6E;animation:pulse 2s ease-in-out infinite;box-shadow:0 0 8px rgba(74,139,110,0.37)} | ||
| 930 | + .nav-sep{width:1px;height:16px;background:#2A2A27} | ||
| 931 | + .nav-logo{height:88px;display:flex;align-items:center;flex-shrink:0} | ||
| 932 | + .nav-logo img{height:100%;width:auto;object-fit:contain} | ||
| 933 | + | ||
| 934 | + /* ── HERO ── */ | ||
| 935 | + .landing-screen{height:100vh;background:#1C1C1A;display:flex;flex-direction:column;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} | ||
| 936 | + .hero{background:#1C1C1A;color:#F5F0E8;flex:1;display:flex;flex-direction:column;justify-content:center;padding:40px 48px 32px;position:relative;overflow:hidden} | ||
| 937 | + .dot-pattern{position:absolute;inset:0;opacity:0.02;background-image:radial-gradient(circle at 1px 1px,#B8B5AD 0.5px,transparent 0);background-size:40px 40px} | ||
| 938 | + .dot-pattern-sm{position:absolute;inset:0;opacity:0.015;background-image:radial-gradient(circle at 1px 1px,#fff 0.5px,transparent 0);background-size:32px 32px} | ||
| 939 | + .hero-inner{max-width:1060px;margin:0 auto;width:100%;position:relative;margin-top:-80px;text-align:center} | ||
| 940 | + .hero-identity{margin-bottom:20px} | ||
| 941 | + .hero-search{display:flex;justify-content:center;position:relative;z-index:50} | ||
| 942 | + .hero-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.25em;color:#B8B5AD} | ||
| 943 | + .hero-title{font-size:clamp(44px,5.5vw,72px);font-weight:900;line-height:1.02;letter-spacing:-0.035em;max-width:720px;margin:14px auto 0} | ||
| 944 | + .hero-sub{font-family:var(--sans);font-size:clamp(16px,1.6vw,18px);color:#A5A29A;max-width:100%;margin-top:14px;line-height:1.6} | ||
| 945 | + .hero-sub-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(165,162,154,0.5);transition:all 0.2s;padding-bottom:1px} | ||
| 946 | + .hero-sub-link:hover{color:#4A8B6E;border-bottom-color:#4A8B6E} | ||
| 947 | + .hero-stats{font-family:var(--mono);font-size:13px;letter-spacing:0.04em;color:#B8B5AD;margin-top:16px;line-height:1} | ||
| 948 | + .stat-num{color:#F5F0E8} | ||
| 949 | + .stat-dot{margin:0 10px;opacity:0.3} | ||
| 950 | + .stat-link{color:inherit;text-decoration:none;border-bottom:1px dotted rgba(184,181,173,0.4);transition:all 0.2s;padding-bottom:1px} | ||
| 951 | + .stat-link:hover{color:#C9A751;border-bottom-color:#C9A751} | ||
| 952 | + | ||
| 953 | + /* Search */ | ||
| 954 | + .search-wrap{max-width:960px;width:100%;margin:0 auto;position:relative;z-index:9999} | ||
| 955 | + .search-bar{display:flex;align-items:center;background:rgba(255,255,255,0.04);border-radius:16px;padding:8px 14px 8px 8px;transition:all 0.4s cubic-bezier(0.16,1,0.3,1);box-shadow:0 0 0 1px rgba(201,167,81,0.12),0 8px 32px rgba(0,0,0,0.25);position:relative} | ||
| 956 | + .search-focused{background:rgba(255,255,255,0.07);box-shadow:0 0 0 1.5px rgba(201,167,81,0.35),0 8px 40px rgba(0,0,0,0.3)} | ||
| 957 | + .search-has-results{border-radius:16px 16px 0 0} | ||
| 958 | + .search-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;padding:0 4px} | ||
| 959 | + .search-meta-item{display:flex;align-items:center;gap:7px;font-family:var(--sans);font-size:13px;font-weight:500;padding:10px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} | ||
| 960 | + .search-meta-item svg{display:block;flex-shrink:0} | ||
| 961 | + .search-badge{color:#4A8B6E;background:rgba(74,139,110,0.12)} | ||
| 962 | + .search-diario-link{color:#C9A751;background:rgba(201,167,81,0.12)} | ||
| 963 | + .search-diario-link:hover{background:rgba(201,167,81,0.22);color:#D4B65E} | ||
| 964 | + .search-diario-link svg{transition:transform 0.2s} | ||
| 965 | + .search-diario-link:hover svg{transform:translateX(3px)} | ||
| 966 | + .search-divider{width:1px;height:32px;background:rgba(255,255,255,0.08);margin:0 16px;flex-shrink:0} | ||
| 967 | + .search-icon{flex-shrink:0;transition:stroke 0.3s} | ||
| 968 | + .search-input-wrap{flex:1;position:relative;display:flex;align-items:center} | ||
| 969 | + .search-input-wrap input{width:100%;background:transparent;border:none;outline:none;color:#F5F0E8;font-size:16px;font-family:var(--sans);padding:12px 12px 12px 10px;caret-color:#C9A751} | ||
| 970 | + .search-placeholder{position:absolute;left:10px;top:50%;transform:translateY(-50%);display:flex;align-items:center;pointer-events:none} | ||
| 971 | + .blink-cursor{width:1.5px;height:18px;margin-right:1px;animation:blink 1s step-end infinite;border-radius:1px;flex-shrink:0} | ||
| 972 | + .cursor-diario{background:#4A8B6E} | ||
| 973 | + .cursor-archivo{background:#C9A751} | ||
| 974 | + .placeholder-text{font-family:var(--sans);font-size:16px;color:#B8B5AD} | ||
| 975 | + .placeholder-mobile{display:none} | ||
| 976 | + .gear-btn{width:38px;height:38px;border-radius:10px;display:flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.06);cursor:pointer;transition:all 0.3s;flex-shrink:0;margin-left:8px} | ||
| 977 | + .gear-btn svg{transition:transform 0.3s ease} | ||
| 978 | + .gear-btn:hover svg{transform:rotate(45deg)} | ||
| 979 | + .gear-btn:hover,.gear-active{background:rgba(255,255,255,0.12)} | ||
| 980 | + .gear-wrap{position:relative} | ||
| 981 | + .gear-desktop{display:block} | ||
| 982 | + .gear-menu{position:absolute;top:calc(100% + 6px);right:0;background:rgba(28,28,26,0.92);backdrop-filter:blur(32px) saturate(1.2);-webkit-backdrop-filter:blur(32px) saturate(1.2);border-radius:16px;min-width:320px;z-index:9999;box-shadow:0 12px 48px rgba(0,0,0,0.5);padding:4px;animation:gearFadeIn 0.15s ease-out} | ||
| 983 | + @keyframes gearFadeIn{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}} | ||
| 984 | + .gear-menu-title{font-family:var(--sans);font-size:11px;font-weight:400;color:#6B6860;padding:10px 14px 6px;letter-spacing:0.02em} | ||
| 985 | + .gear-row{display:flex;align-items:center;gap:12px;width:100%;background:transparent;border:none;padding:12px 14px;border-radius:12px;cursor:pointer;transition:background 0.15s;text-align:left} | ||
| 986 | + .gear-row:hover{background:rgba(255,255,255,0.04)} | ||
| 987 | + .gear-check{width:18px;height:18px;border-radius:6px;border:1.5px solid rgba(255,255,255,0.15);background:transparent;display:flex;align-items:center;justify-content:center;transition:all 0.2s;flex-shrink:0;color:transparent} | ||
| 988 | + .gear-check-on{border-color:transparent;background:#C9A751;color:#1C1C1A} | ||
| 989 | + .gear-row-content{flex:1;display:flex;align-items:baseline;gap:8px} | ||
| 990 | + .gear-row-question{font-family:var(--sans);font-size:12px;color:#5A5850;font-weight:400} | ||
| 991 | + .gear-row-label{font-family:var(--sans);font-size:14px;color:#8B8880;transition:color 0.15s} | ||
| 992 | + .gear-row-label-on{color:#F5F0E8} | ||
| 993 | + .gear-row-count{font-family:var(--mono);font-size:10px;color:#4A4840} | ||
| 994 | + | ||
| 995 | + /* Mobile settings button (hidden on desktop) */ | ||
| 996 | + .mobile-settings-btn{display:none;width:40px;height:40px;border-radius:10px;background:rgba(255,255,255,0.06);border:none;cursor:pointer;align-items:center;justify-content:center;position:relative;transition:all 0.3s;flex-shrink:0} | ||
| 997 | + .mobile-settings-btn:hover,.mobile-settings-open{background:rgba(255,255,255,0.12)} | ||
| 998 | + .mobile-mode-indicator{position:absolute;top:6px;right:6px;width:6px;height:6px;border-radius:50%} | ||
| 999 | + | ||
| 1000 | + /* Mobile settings panel (hidden on desktop) */ | ||
| 1001 | + .mobile-settings-panel{display:none;overflow:hidden;max-height:0;opacity:0;transition:all 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1002 | + .mobile-settings-panel-open{max-height:300px;opacity:1;margin-top:12px} | ||
| 1003 | + .mobile-meta-bar{display:flex;gap:10px;padding:4px 0} | ||
| 1004 | + .mobile-meta-item{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;font-family:var(--sans);font-size:13px;font-weight:500;padding:12px 14px;border-radius:10px;text-decoration:none;transition:all 0.2s} | ||
| 1005 | + .mobile-meta-item svg{flex-shrink:0} | ||
| 1006 | + .mobile-search-badge{color:#4A8B6E;background:rgba(74,139,110,0.12)} | ||
| 1007 | + .mobile-diario-link{color:#C9A751;background:rgba(201,167,81,0.12)} | ||
| 1008 | + .mobile-diario-link:hover,.mobile-diario-link:active{background:rgba(201,167,81,0.22)} | ||
| 1009 | + .mobile-filter-section{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.06)} | ||
| 1010 | + .mobile-filter-title{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.1em;color:#6B6860;text-transform:uppercase;margin-bottom:8px} | ||
| 1011 | + .mobile-filter-bar{display:flex;gap:8px;flex-wrap:wrap} | ||
| 1012 | + .mobile-filter-chip{display:flex;align-items:center;gap:6px;padding:8px 12px;border-radius:20px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);cursor:pointer;transition:all 0.2s;font-family:var(--sans);font-size:13px;color:#6B6860} | ||
| 1013 | + .mobile-filter-chip.filter-chip-active{background:rgba(255,255,255,0.1);border-color:rgba(255,255,255,0.15);color:#F5F0E8} | ||
| 1014 | + .filter-chip-dot{width:6px;height:6px;border-radius:50%;opacity:0.4;transition:opacity 0.2s} | ||
| 1015 | + .mobile-filter-chip.filter-chip-active .filter-chip-dot{opacity:1} | ||
| 1016 | + | ||
| 1017 | + /* Search clear button */ | ||
| 1018 | + .search-clear{background:none;border:none;cursor:pointer;color:#6B6860;padding:4px;display:flex;align-items:center;justify-content:center;transition:color 0.2s;flex-shrink:0} | ||
| 1019 | + .search-clear:hover{color:#F5F0E8} | ||
| 1020 | + | ||
| 1021 | + /* Search Results Dropdown */ | ||
| 1022 | + .search-results{position:absolute;top:100%;left:0;right:0;background:rgba(28,28,26,0.98);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);border-top:none;border-radius:0 0 16px 16px;max-height:320px;overflow-y:auto;z-index:9999} | ||
| 1023 | + .search-loading,.search-no-results{padding:16px 20px;font-family:var(--sans);font-size:14px;color:#9B9890;text-align:center} | ||
| 1024 | + .search-error{margin-top:8px;font-family:var(--sans);font-size:13px;color:#C44B3F;text-align:center} | ||
| 1025 | + | ||
| 1026 | + .search-result-item{width:100%;text-align:left;background:none;border:none;border-top:1px solid rgba(255,255,255,0.06);padding:14px 20px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;transition:background 0.2s} | ||
| 1027 | + .search-result-item:first-child{border-top:none} | ||
| 1028 | + .search-result-item:hover,.result-selected{background:rgba(255,255,255,0.06)} | ||
| 1029 | + | ||
| 1030 | + .result-main{display:flex;align-items:center;gap:8px;flex:1;min-width:0} | ||
| 1031 | + .result-name{font-family:var(--sans);font-size:14px;color:#F5F0E8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | ||
| 1032 | + .result-sigla{font-family:var(--sans);font-size:12px;color:#9B9890;flex-shrink:0} | ||
| 1033 | + | ||
| 1034 | + .result-meta{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:12px} | ||
| 1035 | + .result-code{font-family:var(--mono);font-size:10px;color:#9B9890;background:rgba(255,255,255,0.06);padding:2px 6px;border-radius:4px} | ||
| 1036 | + .result-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;font-weight:600} | ||
| 1037 | + .pills{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap;align-items:center;justify-content:center} | ||
| 1038 | + .pills-label{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#B8B5AD;margin-right:2px} | ||
| 1039 | + .pill{font-family:var(--mono);font-size:12px;font-weight:300;letter-spacing:0.03em;border:none;border-radius:6px;padding:5px 12px;cursor:pointer;transition:all 0.25s;line-height:1.4;position:relative;overflow:hidden;text-decoration:none} | ||
| 1040 | + .pill::before{content:'';position:absolute;inset:0;background:rgba(255,255,255,0.12);opacity:0;transition:opacity 0.25s} | ||
| 1041 | + .pill:hover::before{opacity:1} | ||
| 1042 | + .pill:hover{transform:translateY(-1px)} | ||
| 1043 | + | ||
| 1044 | + .scroll-hint{position:absolute;bottom:28px;left:0;right:0;margin:0 auto;width:fit-content;display:flex;flex-direction:column;align-items:center;gap:10px;animation:scrollHint 2.5s ease-in-out infinite;z-index:1;transition:opacity 0.3s} | ||
| 1045 | + .scroll-hint-hidden{opacity:0;pointer-events:none} | ||
| 1046 | + .scroll-hint span{font-family:var(--mono);font-size:10px;letter-spacing:0.2em;color:#B8B5AD;opacity:0.7} | ||
| 1047 | + | ||
| 1048 | + /* ── DIRECTORY ── */ | ||
| 1049 | + .directory{background:#f5f5f7;min-height:100vh;display:flex;align-items:center;scroll-snap-align:start;scroll-snap-stop:always;padding:48px} | ||
| 1050 | + .directory-inner{display:grid;grid-template-columns:280px 1fr;gap:48px;width:100%;max-width:1200px;margin:0 auto;align-items:center} | ||
| 1051 | + .directory-left{} | ||
| 1052 | + .directory-right{display:flex;flex-direction:column;gap:20px} | ||
| 1053 | + .section-tag{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#6B6860;margin-bottom:10px} | ||
| 1054 | + .section-tag-light{font-family:var(--mono);font-size:12px;letter-spacing:0.2em;color:#B8B5AD} | ||
| 1055 | + .section-title{font-size:clamp(40px,4.5vw,56px);font-weight:900;letter-spacing:-0.025em;line-height:1.05;color:#1C1C1A} | ||
| 1056 | + .section-title-light{font-size:clamp(32px,4vw,46px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8} | ||
| 1057 | + | ||
| 1058 | + /* Door cards */ | ||
| 1059 | + .door{border-radius:24px;padding:44px 40px 40px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;display:flex;flex-direction:column;justify-content:space-between;min-height:260px} | ||
| 1060 | + .door-hovered{transform:translateY(-4px)} | ||
| 1061 | + .door-dark{background:#1C1C1A;color:#F5F0E8;box-shadow:none} | ||
| 1062 | + .door-dark.door-hovered{box-shadow:0 24px 64px rgba(0,0,0,0.2)} | ||
| 1063 | + .door-warm{background:#ffffff;color:#1C1C1A;box-shadow:none} | ||
| 1064 | + .door-warm.door-hovered{box-shadow:0 24px 64px rgba(201,167,81,0.12)} | ||
| 1065 | + .door-accent{position:absolute;top:0;left:0;right:0;height:3px;transform-origin:left;transition:transform 0.6s cubic-bezier(0.16,1,0.3,1)} | ||
| 1066 | + .door-content{position:relative} | ||
| 1067 | + .door-header{display:flex;align-items:center;gap:10px;margin-bottom:16px} | ||
| 1068 | + .door-icon{transition:opacity 0.3s} | ||
| 1069 | + .door-badge{display:flex;align-items:center;gap:6px} | ||
| 1070 | + .door-badge-text{font-family:var(--mono);font-size:11px;letter-spacing:0.16em;font-weight:500} | ||
| 1071 | + .door-title{font-size:clamp(28px,3vw,38px);font-weight:900;letter-spacing:-0.02em;line-height:1.08;color:#F5F0E8} | ||
| 1072 | + .door-title-dark{color:#1C1C1A} | ||
| 1073 | + .door-desc{font-family:var(--sans);font-size:17px;color:#6B6860;line-height:1.55;margin-top:10px;max-width:480px} | ||
| 1074 | + .door-desc-light{color:#9B9890} | ||
| 1075 | + .door-pills{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px} | ||
| 1076 | + .door-pill{font-family:var(--sans);font-size:16px;padding:10px 16px;border-radius:8px;line-height:1.3} | ||
| 1077 | + .door-pill-dark{background:rgba(74,139,110,0.12);color:#8ac4a8;font-weight:500} | ||
| 1078 | + .door-pill-light{background:rgba(166,138,62,0.1);color:#a68a3e;font-weight:500} | ||
| 1079 | + .door-pill-green{background:rgba(74,139,110,0.1);color:#3d7a5e;font-weight:500} | ||
| 1080 | + .door-footer{position:relative;display:flex;justify-content:space-between;align-items:center;margin-top:24px} | ||
| 1081 | + .door-meta{font-family:var(--mono);font-size:11px;letter-spacing:0.1em;color:#4A4843} | ||
| 1082 | + .door-meta-light{color:#B8B5AD} | ||
| 1083 | + .door-cta{font-family:var(--sans);font-size:16px;font-weight:400;transition:color 0.3s} | ||
| 1084 | + | ||
| 1085 | + /* ── CLASIFICADORES ── */ | ||
| 1086 | + .clasificadores{background:#e8e8ea;color:#1C1C1A;padding:48px;height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always;overflow:hidden} | ||
| 1087 | + .clas-container{max-width:1100px;margin:0 auto;width:100%;max-height:calc(100vh - 96px);display:flex;flex-direction:column} | ||
| 1088 | + .clas-header-section{margin-bottom:24px;flex-shrink:0} | ||
| 1089 | + .clas-header-row{display:flex;justify-content:space-between;align-items:flex-end} | ||
| 1090 | + .clas-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px} | ||
| 1091 | + .clas-ver-todos{font-family:var(--sans);font-size:15px;font-weight:500;color:#1C1C1A;transition:color 0.2s} | ||
| 1092 | + .clas-ver-todos:hover{color:#4A8B6E} | ||
| 1093 | + | ||
| 1094 | + /* Group cards */ | ||
| 1095 | + .clas-group-card{background:#f2f2f4;border:1px solid rgba(0,0,0,0.04);border-radius:20px;padding:16px;margin-bottom:12px;height:100%;display:flex;flex-direction:column;transition:all 0.3s} | ||
| 1096 | + /* Parent reacts to child hover */ | ||
| 1097 | + .clas-group-card:has(.widget-card:hover){background:#efefef;border-color:rgba(0,0,0,0.06)} | ||
| 1098 | + .clas-group-card:has(.widget-card:hover) > .clas-group-header .clas-group-label{transform:scale(1.05);transform-origin:left} | ||
| 1099 | + /* Parent hover highlights children */ | ||
| 1100 | + .clas-group-card:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)} | ||
| 1101 | + .clas-group-card:hover .clas-subgroup{background:#f5f5f7} | ||
| 1102 | + .gastos-content{display:flex;flex-direction:column;flex:1;gap:10px} | ||
| 1103 | + .gastos-content .clas-subgroup{flex:1;margin-bottom:0} | ||
| 1104 | + .gastos-content > .widgets-row{flex:1} | ||
| 1105 | + .clas-group-header{display:flex;align-items:center;gap:10px;margin-bottom:10px} | ||
| 1106 | + .clas-group-icon{width:32px;height:32px;border-radius:10px;background:color-mix(in srgb, var(--c) 10%, transparent);border:1px solid color-mix(in srgb, var(--c) 15%, transparent);display:flex;align-items:center;justify-content:center;color:var(--c);flex-shrink:0} | ||
| 1107 | + .clas-group-label{font-family:var(--mono);font-size:12px;letter-spacing:0.15em;font-weight:600;color:#1C1C1A;transition:transform 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1108 | + .clas-group-sub{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px} | ||
| 1109 | + | ||
| 1110 | + /* Subgroup */ | ||
| 1111 | + .clas-subgroup{background:#f7f7f9;border:1px solid rgba(0,0,0,0.03);border-radius:14px;padding:12px;margin-bottom:10px;display:flex;flex-direction:column;transition:all 0.3s} | ||
| 1112 | + /* Subgroup parent reacts to child hover */ | ||
| 1113 | + .clas-subgroup:has(.widget-card:hover){background:#f3f3f5;border-color:rgba(0,0,0,0.05)} | ||
| 1114 | + .clas-subgroup:has(.widget-card:hover) > .clas-subgroup-header .clas-subgroup-label{transform:scale(1.05);transform-origin:left} | ||
| 1115 | + /* Subgroup hover highlights children */ | ||
| 1116 | + .clas-subgroup:hover .widget-card{background:#fdfdfd;border-color:rgba(0,0,0,0.05)} | ||
| 1117 | + .clas-subgroup .widgets-row{flex:1} | ||
| 1118 | + .clas-subgroup-header{display:flex;align-items:center;gap:10px;margin-bottom:10px} | ||
| 1119 | + .clas-subgroup-icon{width:32px;height:32px;border-radius:10px;background:color-mix(in srgb, var(--c) 10%, transparent);border:1px solid color-mix(in srgb, var(--c) 15%, transparent);display:flex;align-items:center;justify-content:center;color:var(--c);flex-shrink:0} | ||
| 1120 | + .clas-subgroup-label{font-family:var(--mono);font-size:12px;letter-spacing:0.15em;font-weight:600;color:#1C1C1A;transition:transform 0.3s cubic-bezier(0.16,1,0.3,1)} | ||
| 1121 | + .clas-subgroup-tag{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:2px} | ||
| 1122 | + | ||
| 1123 | + /* Main grid */ | ||
| 1124 | + .clas-main-grid{display:grid;grid-template-columns:1fr 2fr;gap:12px;align-items:stretch;flex:1;min-height:0} | ||
| 1125 | + .clas-col{display:flex;flex-direction:column} | ||
| 1126 | + .clas-col .clas-group-card{flex:1} | ||
| 1127 | + | ||
| 1128 | + /* Widget cards */ | ||
| 1129 | + .widget-card{display:block;background:#fafafa;border:1px solid rgba(0,0,0,0.04);border-radius:14px;padding:14px;cursor:pointer;text-decoration:none;color:#1C1C1A;transition:all 0.35s cubic-bezier(0.16,1,0.3,1);box-shadow:0 1px 3px rgba(0,0,0,0.02)} | ||
| 1130 | + .widget-hover{background:#ffffff;border-color:rgba(0,0,0,0.06);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,0.05)} | ||
| 1131 | + .widget-h{display:flex;justify-content:space-between;align-items:center} | ||
| 1132 | + .widget-left{flex:1} | ||
| 1133 | + .widget-right{display:flex;align-items:center;gap:12px;flex-shrink:0} | ||
| 1134 | + .widget-grow{flex:1} | ||
| 1135 | + .widget-sm{padding:12px} | ||
| 1136 | + .widget-full{flex:1} | ||
| 1137 | + .widgets-col{display:flex;flex-direction:column;gap:8px;flex:1} | ||
| 1138 | + .widgets-col .widget-card{flex:1} | ||
| 1139 | + .widgets-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;flex:1} | ||
| 1140 | + .widgets-row .widget-card{display:flex;flex-direction:column;justify-content:space-between} | ||
| 1141 | + .widget-headline{display:flex;align-items:baseline;gap:8px;margin-bottom:6px} | ||
| 1142 | + .widget-num-big{font-family:var(--mono);font-size:24px;font-weight:700;line-height:1} | ||
| 1143 | + .widget-num-gold{color:#a68a3e} | ||
| 1144 | + .widget-num-green{color:#4A8B6E} | ||
| 1145 | + .widget-label{font-family:var(--sans);font-size:14px;font-weight:700;color:#1C1C1A;line-height:1.2} | ||
| 1146 | + .widget-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.4;font-weight:400} | ||
| 1147 | + .widget-desc-sm{font-size:13px} | ||
| 1148 | + .widget-footer{display:flex;justify-content:space-between;align-items:center;margin-top:8px} | ||
| 1149 | + .widget-tech{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;color:#9B9890;text-transform:uppercase} | ||
| 1150 | + .widget-arrow{font-family:var(--sans);font-size:14px;color:transparent;transform:translateX(-6px);transition:all 0.35s cubic-bezier(0.16,1,0.3,1)} | ||
| 1151 | + .arrow-show{color:#1C1C1A;transform:translateX(0)} | ||
| 1152 | + | ||
| 1153 | + /* ── HISTORIAS ── */ | ||
| 1154 | + .historias{background:#1C1C1A;padding:80px 48px;color:#F5F0E8;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1155 | + .historias .container{width:100%;max-width:1060px;margin:0 auto} | ||
| 1156 | + .hist-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px} | ||
| 1157 | + .hist-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px} | ||
| 1158 | + .hist-tag-row svg{stroke:#9B9890} | ||
| 1159 | + .hist-tag-row .section-tag{color:#9B9890} | ||
| 1160 | + .historias .section-title{color:#F5F0E8} | ||
| 1161 | + .hist-subtitle{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:10px} | ||
| 1162 | + .hist-grid{display:grid;grid-template-columns:repeat(3, 1fr);grid-auto-rows:minmax(120px, auto);gap:16px} | ||
| 1163 | + .hist-cell{grid-row:span 1} | ||
| 1164 | + .hist-tall{grid-row:span 2} | ||
| 1165 | + .hist-card{background:color-mix(in srgb, var(--card-color) 15%, #1C1C1A);border:1px solid color-mix(in srgb, var(--card-color) 25%, transparent);border-radius:18px;padding:28px 24px 24px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;height:100%;display:flex;flex-direction:column;justify-content:space-between} | ||
| 1166 | + .hist-card-hovered{background:color-mix(in srgb, var(--card-color) 22%, #1C1C1A);border-color:color-mix(in srgb, var(--card-color) 35%, transparent);transform:translateY(-3px)} | ||
| 1167 | + .hist-accent{position:absolute;top:0;left:0;right:0;height:2px;transform-origin:left;transition:transform 0.5s cubic-bezier(0.16,1,0.3,1)} | ||
| 1168 | + .hist-content{flex:1} | ||
| 1169 | + .hist-tag{font-family:var(--mono);font-size:10px;letter-spacing:0.14em;font-weight:500} | ||
| 1170 | + .hist-title{font-family:var(--serif);font-size:18px;font-weight:800;margin-top:12px;line-height:1.25;letter-spacing:-0.01em;color:#F5F0E8} | ||
| 1171 | + .hist-tall .hist-title{font-size:22px} | ||
| 1172 | + .hist-desc{font-family:var(--sans);font-size:14px;color:#9B9890;line-height:1.5;margin-top:10px} | ||
| 1173 | + .hist-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860} | ||
| 1174 | + .hist-arrow{transition:transform 0.3s;display:inline-block} | ||
| 1175 | + .hist-arrow-moved{transform:translateX(3px)} | ||
| 1176 | + | ||
| 1177 | + /* ── VISUALIZACIONES ── */ | ||
| 1178 | + .visualizaciones{background:#f5f5f7;padding:80px 48px;color:#1C1C1A;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1179 | + .visualizaciones .container{width:100%;max-width:1060px;margin:0 auto} | ||
| 1180 | + .vis-header{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:36px} | ||
| 1181 | + .vis-tag-row{display:flex;align-items:center;gap:8px;margin-bottom:8px} | ||
| 1182 | + .vis-subtitle{font-family:var(--sans);font-size:17px;color:#6B6860;margin-top:10px} | ||
| 1183 | + .gold-link{font-family:var(--sans);font-size:15px;color:#C9A751;font-weight:600} | ||
| 1184 | + .vis-grid-simple{display:grid;grid-template-columns:repeat(3, 1fr);gap:16px} | ||
| 1185 | + .vis-card{background:#fafafc;border-radius:18px;padding:28px 24px 24px;cursor:pointer;transition:all 0.5s cubic-bezier(0.16,1,0.3,1);position:relative;overflow:hidden;height:100%;display:flex;flex-direction:column;justify-content:space-between} | ||
| 1186 | + .vis-card-hovered{transform:translateY(-2px);box-shadow:0 16px 40px rgba(0,0,0,0.05)} | ||
| 1187 | + .vis-accent{position:absolute;top:0;left:0;right:0;height:2px;transform-origin:left;transition:transform 0.5s cubic-bezier(0.16,1,0.3,1)} | ||
| 1188 | + .vis-type{font-family:var(--mono);font-size:10px;letter-spacing:0.1em;font-weight:500;padding:5px 10px;border-radius:6px;display:inline-block} | ||
| 1189 | + .vis-title{font-family:var(--serif);font-size:18px;font-weight:800;margin-top:12px;line-height:1.25;letter-spacing:-0.01em;color:#1C1C1A} | ||
| 1190 | + .vis-desc{font-family:var(--sans);font-size:14px;color:#6B6860;line-height:1.5;margin-top:10px} | ||
| 1191 | + .vis-cta{font-family:var(--sans);font-size:14px;margin-top:20px;transition:color 0.3s;display:flex;align-items:center;gap:3px;color:#6B6860} | ||
| 1192 | + .vis-arrow{transition:transform 0.3s;display:inline-block} | ||
| 1193 | + .vis-arrow-moved{transform:translateX(3px)} | ||
| 1194 | + .vis-card-link{text-decoration:none;color:inherit} | ||
| 1195 | + .vis-card-disabled{opacity:0.6;cursor:default} | ||
| 1196 | + .vis-card-disabled:hover{transform:none;box-shadow:none} | ||
| 1197 | + | ||
| 1198 | + /* ── MANIFIESTO ── */ | ||
| 1199 | + .manifiesto{background:#1C1C1A;color:#F5F0E8;padding:96px 48px;position:relative;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1200 | + .manifiesto-inner{max-width:640px;width:100%;margin:0 auto;position:relative;text-align:center} | ||
| 1201 | + .manifiesto-title{font-size:clamp(36px,4.5vw,56px);font-weight:900;line-height:1.08;letter-spacing:-0.025em;margin-top:18px} | ||
| 1202 | + .manifiesto-text{font-family:var(--sans);font-size:17px;color:#9B9890;margin-top:20px;line-height:1.7} | ||
| 1203 | + | ||
| 1204 | + /* ── DESCARGAS ── */ | ||
| 1205 | + .descargas{background:#1C1C1A;padding:80px 48px;color:#F5F0E8;min-height:100vh;display:flex;flex-direction:column;justify-content:center;scroll-snap-align:start;scroll-snap-stop:always} | ||
| 1206 | + .descargas-inner{max-width:640px;width:100%;margin:0 auto;text-align:center} | ||
| 1207 | + .descargas-title{font-size:clamp(32px,4vw,48px);font-weight:900;line-height:1.1;letter-spacing:-0.02em;color:#F5F0E8} | ||
| 1208 | + .descargas-formats{display:flex;justify-content:center;gap:12px;margin-top:24px} | ||
| 1209 | + .format-pill{font-family:var(--mono);font-size:14px;letter-spacing:0.05em;color:#9B9890;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);padding:8px 16px;border-radius:8px} | ||
| 1210 | + .descargas-link{display:inline-flex;align-items:center;gap:8px;margin-top:32px;font-family:var(--sans);font-size:15px;color:#C9A751;transition:all 0.3s} | ||
| 1211 | + .descargas-link:hover{color:#d4b45a;gap:10px} | ||
| 1212 | + | ||
| 1213 | + /* ── FOOTER ── */ | ||
| 1214 | + footer{background:#1C1C1A;padding:40px 48px;border-top:1px solid #2A2A27} | ||
| 1215 | + .footer-inner{max-width:1060px;margin:0 auto;display:flex;justify-content:space-between;align-items:center} | ||
| 1216 | + .footer-org{font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:#6B6860} | ||
| 1217 | + .footer-unit{font-family:var(--sans);font-size:13px;color:#6B6860;margin-top:3px} | ||
| 1218 | + .footer-right{font-family:var(--mono);font-size:12px;letter-spacing:0.1em;color:#6B6860} | ||
| 1219 | + | ||
| 1220 | + /* ══════════════════════════════════════════════════════════════════════════ | ||
| 1221 | + RESPONSIVE - TABLET (768px) | ||
| 1222 | + ══════════════════════════════════════════════════════════════════════════ */ | ||
| 1223 | + @media(max-width:768px){ | ||
| 1224 | + .page{scroll-snap-type:y proximity} | ||
| 1225 | + .landing-screen,.directory,.clasificadores,.historias,.visualizaciones,.manifiesto,.descargas{scroll-snap-stop:normal} | ||
| 1226 | + | ||
| 1227 | + /* Section dots - bottom horizontal bar */ | ||
| 1228 | + .section-dots{position:fixed;right:auto;left:50%;top:auto;bottom:20px;transform:translateX(-50%);flex-direction:row;gap:10px;padding:12px 20px;border-radius:24px;background:rgba(28,28,26,0.7);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid rgba(255,255,255,0.1);z-index:1000} | ||
| 1229 | + .section-dot{width:8px;height:8px} | ||
| 1230 | + .section-dot-label{display:none} | ||
| 1231 | + | ||
| 1232 | + /* Nav */ | ||
| 1233 | + nav{padding:12px 24px} | ||
| 1234 | + .nav-logo{height:60px} | ||
| 1235 | + | ||
| 1236 | + /* Hero */ | ||
| 1237 | + .hero{padding:32px 24px} | ||
| 1238 | + .hero-inner{margin-top:0} | ||
| 1239 | + .hero-title{font-size:clamp(36px,8vw,48px)} | ||
| 1240 | + .hero-stats{font-size:11px;line-height:1.6} | ||
| 1241 | + .stat-dot{margin:0 6px} | ||
| 1242 | + | ||
| 1243 | + /* Search bar - mobile compact */ | ||
| 1244 | + .search-bar{flex-wrap:nowrap;padding:8px 12px;gap:8px} | ||
| 1245 | + .toggle-desktop{display:none} | ||
| 1246 | + .search-divider{display:none} | ||
| 1247 | + .gear-desktop{display:none} | ||
| 1248 | + .mobile-settings-btn{display:flex} | ||
| 1249 | + .mobile-settings-panel{display:block} | ||
| 1250 | + .search-icon{margin-left:0} | ||
| 1251 | + .search-input-wrap{order:1} | ||
| 1252 | + .search-input-wrap input{font-size:15px;padding:10px 8px} | ||
| 1253 | + .placeholder-desktop{display:none} | ||
| 1254 | + .placeholder-mobile{display:inline;font-size:14px} | ||
| 1255 | + | ||
| 1256 | + /* Search results mobile */ | ||
| 1257 | + .search-results{max-height:260px;border-radius:0 0 12px 12px} | ||
| 1258 | + .search-result-item{padding:12px 16px} | ||
| 1259 | + .result-name{font-size:13px} | ||
| 1260 | + .result-meta{flex-direction:column;align-items:flex-end;gap:4px} | ||
| 1261 | + .result-code{font-size:9px} | ||
| 1262 | + .result-type{font-size:8px} | ||
| 1263 | + .pills{gap:6px} | ||
| 1264 | + .pill{font-size:11px;padding:4px 10px} | ||
| 1265 | + .scroll-hint{bottom:20px} | ||
| 1266 | + | ||
| 1267 | + /* Directory */ | ||
| 1268 | + .directory{padding:48px 24px} | ||
| 1269 | + .directory-inner{grid-template-columns:1fr;gap:32px} | ||
| 1270 | + .directory-left{text-align:center} | ||
| 1271 | + .section-title{font-size:clamp(32px,7vw,42px)} | ||
| 1272 | + .door{padding:28px 24px 24px;min-height:auto} | ||
| 1273 | + .door-title{font-size:clamp(24px,5vw,32px)} | ||
| 1274 | + .door-pills{gap:6px} | ||
| 1275 | + .door-pill{font-size:14px;padding:8px 12px} | ||
| 1276 | + .door-meta{font-size:10px} | ||
| 1277 | + | ||
| 1278 | + /* Clasificadores */ | ||
| 1279 | + .clasificadores{padding:32px 20px;height:auto;min-height:100vh;overflow-y:auto} | ||
| 1280 | + .clas-container{max-height:none} | ||
| 1281 | + .clas-header-row{flex-direction:column;align-items:flex-start;gap:12px} | ||
| 1282 | + .clas-ver-todos{align-self:flex-start} | ||
| 1283 | + .clas-main-grid{grid-template-columns:1fr;gap:12px} | ||
| 1284 | + .widgets-row{grid-template-columns:1fr} | ||
| 1285 | + .widget-h{flex-direction:column;align-items:flex-start;gap:12px} | ||
| 1286 | + .widget-right{width:100%;justify-content:space-between} | ||
| 1287 | + .widget-num-big{font-size:22px} | ||
| 1288 | + .clas-group-icon,.clas-subgroup-icon{width:28px;height:28px} | ||
| 1289 | + | ||
| 1290 | + /* Historias */ | ||
| 1291 | + .historias{padding:60px 24px} | ||
| 1292 | + .hist-header{flex-direction:column;align-items:flex-start;gap:16px} | ||
| 1293 | + .hist-grid{grid-template-columns:1fr 1fr;grid-auto-rows:auto;gap:12px} | ||
| 1294 | + .hist-tall{grid-row:span 1} | ||
| 1295 | + .hist-card{padding:20px 18px 18px} | ||
| 1296 | + .hist-title{font-size:16px} | ||
| 1297 | + .hist-tall .hist-title{font-size:18px} | ||
| 1298 | + .hist-desc{font-size:13px} | ||
| 1299 | + | ||
| 1300 | + /* Visualizaciones */ | ||
| 1301 | + .visualizaciones{padding:60px 24px} | ||
| 1302 | + .vis-header{flex-direction:column;align-items:flex-start;gap:16px} | ||
| 1303 | + .vis-grid-simple{grid-template-columns:1fr 1fr;gap:12px} | ||
| 1304 | + .vis-card{padding:20px 18px 18px} | ||
| 1305 | + .vis-title{font-size:16px} | ||
| 1306 | + | ||
| 1307 | + /* Manifiesto */ | ||
| 1308 | + .manifiesto{padding:60px 24px} | ||
| 1309 | + .manifiesto-title{font-size:clamp(28px,6vw,40px)} | ||
| 1310 | + | ||
| 1311 | + /* Descargas */ | ||
| 1312 | + .descargas{padding:60px 24px} | ||
| 1313 | + .descargas-title{font-size:clamp(28px,6vw,40px)} | ||
| 1314 | + .descargas-formats{gap:8px} | ||
| 1315 | + .format-pill{font-size:12px;padding:6px 12px} | ||
| 1316 | + | ||
| 1317 | + /* Footer */ | ||
| 1318 | + footer{padding:32px 24px} | ||
| 1319 | + .footer-inner{flex-direction:column;gap:12px;text-align:center} | ||
| 1320 | + .footer-org,.footer-right{font-size:10px} | ||
| 1321 | + } | ||
| 1322 | + | ||
| 1323 | + /* ══════════════════════════════════════════════════════════════════════════ | ||
| 1324 | + RESPONSIVE - MOBILE (480px) | ||
| 1325 | + ══════════════════════════════════════════════════════════════════════════ */ | ||
| 1326 | + @media(max-width:480px){ | ||
| 1327 | + /* Section dots - bottom bar smaller */ | ||
| 1328 | + .section-dots{bottom:16px;gap:8px;padding:10px 16px;border-radius:20px} | ||
| 1329 | + .section-dot{width:7px;height:7px} | ||
| 1330 | + | ||
| 1331 | + /* Nav */ | ||
| 1332 | + nav{padding:10px 16px} | ||
| 1333 | + .nav-logo{height:48px} | ||
| 1334 | + | ||
| 1335 | + /* Hero */ | ||
| 1336 | + .hero{padding:24px 16px} | ||
| 1337 | + .hero-title{font-size:clamp(32px,9vw,40px)} | ||
| 1338 | + .hero-sub{font-size:15px} | ||
| 1339 | + .hero-stats{font-size:10px;display:flex;flex-wrap:wrap;justify-content:center;gap:4px 0} | ||
| 1340 | + .stat-dot{margin:0 4px} | ||
| 1341 | + | ||
| 1342 | + /* Search */ | ||
| 1343 | + .search-wrap{padding:0} | ||
| 1344 | + .mobile-settings-btn{width:36px;height:36px} | ||
| 1345 | + .mobile-meta-item{padding:10px 12px;font-size:12px} | ||
| 1346 | + .search-results{max-height:220px} | ||
| 1347 | + .search-result-item{padding:10px 14px} | ||
| 1348 | + .result-name{font-size:12px} | ||
| 1349 | + .result-sigla{font-size:10px} | ||
| 1350 | + .pills-label{display:none} | ||
| 1351 | + .pill{font-size:10px;padding:4px 8px} | ||
| 1352 | + .scroll-hint{display:none} | ||
| 1353 | + | ||
| 1354 | + /* Directory */ | ||
| 1355 | + .directory{padding:40px 16px} | ||
| 1356 | + .directory-inner{gap:24px} | ||
| 1357 | + .section-tag{font-size:10px} | ||
| 1358 | + .section-title{font-size:clamp(28px,8vw,36px)} | ||
| 1359 | + .door{padding:24px 20px 20px;border-radius:18px} | ||
| 1360 | + .door-icon svg{width:22px;height:22px} | ||
| 1361 | + .door-title{font-size:clamp(22px,6vw,28px)} | ||
| 1362 | + .door-pill{font-size:13px;padding:7px 10px} | ||
| 1363 | + .door-footer{flex-direction:column;align-items:flex-start;gap:8px} | ||
| 1364 | + .door-meta{font-size:9px} | ||
| 1365 | + .door-cta{font-size:14px} | ||
| 1366 | + | ||
| 1367 | + /* Clasificadores */ | ||
| 1368 | + .clasificadores{padding:24px 16px} | ||
| 1369 | + .clas-header-section{margin-bottom:16px} | ||
| 1370 | + .section-title{font-size:clamp(24px,6vw,32px)} | ||
| 1371 | + .clas-subtitle{font-size:14px} | ||
| 1372 | + .clas-group-card{padding:12px;border-radius:14px;margin-bottom:10px} | ||
| 1373 | + .clas-subgroup{padding:10px;border-radius:10px} | ||
| 1374 | + .clas-group-header,.clas-subgroup-header{margin-bottom:8px} | ||
| 1375 | + .clas-group-label,.clas-subgroup-label{font-size:10px} | ||
| 1376 | + .clas-group-sub{font-size:12px} | ||
| 1377 | + .widget-card{padding:12px;border-radius:10px} | ||
| 1378 | + .widget-num-big{font-size:20px} | ||
| 1379 | + .widget-label{font-size:13px} | ||
| 1380 | + .widget-desc{font-size:12px} | ||
| 1381 | + .widget-tech{font-size:8px} | ||
| 1382 | + | ||
| 1383 | + /* Historias */ | ||
| 1384 | + .historias{padding:48px 16px} | ||
| 1385 | + .hist-subtitle{font-size:14px} | ||
| 1386 | + .hist-grid{grid-template-columns:1fr;gap:10px} | ||
| 1387 | + .hist-card{padding:18px 16px 16px;border-radius:14px} | ||
| 1388 | + .hist-tag{font-size:9px} | ||
| 1389 | + .hist-title{font-size:16px} | ||
| 1390 | + .hist-desc{font-size:12px;margin-top:8px} | ||
| 1391 | + .hist-cta{font-size:13px;margin-top:14px} | ||
| 1392 | + | ||
| 1393 | + /* Visualizaciones */ | ||
| 1394 | + .visualizaciones{padding:48px 16px} | ||
| 1395 | + .vis-subtitle{font-size:14px} | ||
| 1396 | + .vis-grid-simple{grid-template-columns:1fr;gap:10px} | ||
| 1397 | + .vis-card{padding:18px 16px 16px;border-radius:14px} | ||
| 1398 | + .vis-type{font-size:9px;padding:4px 8px} | ||
| 1399 | + .vis-title{font-size:16px} | ||
| 1400 | + .vis-desc{font-size:12px} | ||
| 1401 | + .vis-cta{font-size:13px;margin-top:14px} | ||
| 1402 | + | ||
| 1403 | + /* Manifiesto */ | ||
| 1404 | + .manifiesto{padding:48px 16px} | ||
| 1405 | + .section-tag-light{font-size:10px} | ||
| 1406 | + .manifiesto-title{font-size:clamp(24px,7vw,32px)} | ||
| 1407 | + | ||
| 1408 | + /* Descargas */ | ||
| 1409 | + .descargas{padding:48px 16px} | ||
| 1410 | + .descargas-title{font-size:clamp(24px,7vw,32px)} | ||
| 1411 | + .descargas-formats{flex-wrap:wrap;gap:6px} | ||
| 1412 | + .format-pill{font-size:11px;padding:5px 10px} | ||
| 1413 | + .descargas-link{font-size:14px;margin-top:24px} | ||
| 1414 | + | ||
| 1415 | + /* Footer */ | ||
| 1416 | + footer{padding:24px 16px} | ||
| 1417 | + .footer-org{font-size:9px;letter-spacing:0.08em} | ||
| 1418 | + .footer-right{font-size:9px} | ||
| 1419 | + } | ||
| 1420 | +</style> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 1 | +<script> | ||
| 2 | + import { onMount } from 'svelte'; | ||
| 3 | + | ||
| 4 | + // Estado base | ||
| 5 | + let mounted = false; | ||
| 6 | + let searchFocused = false; | ||
| 7 | + let searchVal = ''; | ||
| 8 | + let searchInput; | ||
| 9 | + let isLoading = false; | ||
| 10 | + let results = null; | ||
| 11 | + let error = null; | ||
| 12 | + | ||
| 13 | + // Ordenamiento | ||
| 14 | + let sortBy = 'amount'; | ||
| 15 | + let sortOrder = 'desc'; | ||
| 16 | + | ||
| 17 | + // Paginación | ||
| 18 | + let currentPage = 1; | ||
| 19 | + let isLoadingMore = false; | ||
| 20 | + let allHits = []; | ||
| 21 | + | ||
| 22 | + function toggleSort(field) { | ||
| 23 | + if (sortBy === field) { | ||
| 24 | + sortOrder = sortOrder === 'desc' ? 'asc' : 'desc'; | ||
| 25 | + } else { | ||
| 26 | + sortBy = field; | ||
| 27 | + sortOrder = 'desc'; | ||
| 28 | + } | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + $: sortedHits = allHits.length > 0 ? [...allHits].sort((a, b) => { | ||
| 32 | + let valA = sortBy === 'year' ? (a.document.gestion || 0) : (a.document.devengado || 0); | ||
| 33 | + let valB = sortBy === 'year' ? (b.document.gestion || 0) : (b.document.devengado || 0); | ||
| 34 | + return sortOrder === 'desc' ? valB - valA : valA - valB; | ||
| 35 | + }) : []; | ||
| 36 | + | ||
| 37 | + $: hasMoreResults = results && allHits.length < results.found; | ||
| 38 | + | ||
| 39 | + // Modo de búsqueda | ||
| 40 | + let searchMode = 'gastos'; // 'gastos' | 'ingresos' | ||
| 41 | + let searchSubmode = 'programas'; // 'programas' | 'clasificadores' | ||
| 42 | + let selectedClassifiers = []; | ||
| 43 | + | ||
| 44 | + const CLASIFICADORES_GASTOS = [ | ||
| 45 | + { id: 'finfun', label: 'Finalidad y función' }, | ||
| 46 | + { id: 'objeto', label: 'Objetos de gasto' }, | ||
| 47 | + { id: 'acteco', label: 'Actividad económica' }, | ||
| 48 | + { id: 'entidad', label: 'Entidades' }, | ||
| 49 | + { id: 'geografico', label: 'Geográfico', disabled: true }, | ||
| 50 | + ]; | ||
| 51 | + | ||
| 52 | + const CLASIFICADORES_INGRESOS = [ | ||
| 53 | + { id: 'rubro', label: 'Rubro', disabled: true }, | ||
| 54 | + { id: 'organismo', label: 'Organismo', disabled: true }, | ||
| 55 | + { id: 'fuente', label: 'Fuente', disabled: true }, | ||
| 56 | + { id: 'entidad', label: 'Entidades' }, | ||
| 57 | + { id: 'geografico',label: 'Geográfico', disabled: true }, | ||
| 58 | + ]; | ||
| 59 | + | ||
| 60 | + $: activeClasificadores = searchMode === 'gastos' ? CLASIFICADORES_GASTOS : CLASIFICADORES_INGRESOS; | ||
| 61 | + | ||
| 62 | + function toggleClassifier(id) { | ||
| 63 | + if (selectedClassifiers.includes(id)) { | ||
| 64 | + selectedClassifiers = selectedClassifiers.filter(c => c !== id); | ||
| 65 | + } else { | ||
| 66 | + selectedClassifiers = [...selectedClassifiers, id]; | ||
| 67 | + } | ||
| 68 | + } | ||
| 69 | + | ||
| 70 | + function setMode(mode) { | ||
| 71 | + searchMode = mode; | ||
| 72 | + selectedClassifiers = []; | ||
| 73 | + if (mode === 'gastos') { | ||
| 74 | + searchSubmode = 'programas'; | ||
| 75 | + } else { | ||
| 76 | + searchSubmode = 'clasificadores'; | ||
| 77 | + } | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + function setSubmode(sub) { | ||
| 81 | + searchSubmode = sub; | ||
| 82 | + selectedClassifiers = []; | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + // Placeholder dinámico | ||
| 86 | + $: placeholderText = getPlaceholderText(searchMode, searchSubmode, selectedClassifiers); | ||
| 87 | + | ||
| 88 | + function getPlaceholderText(mode, submode, classifiers) { | ||
| 89 | + if (mode === 'gastos') { | ||
| 90 | + if (submode === 'programas') return 'Buscar en programas y proyectos...'; | ||
| 91 | + if (classifiers.length === 0) return 'Selecciona al menos un clasificador...'; | ||
| 92 | + if (classifiers.length === CLASIFICADORES_GASTOS.length) return 'Buscar en todos los clasificadores de gasto...'; | ||
| 93 | + return `Buscar en ${classifiers.length} clasificador${classifiers.length > 1 ? 'es' : ''} de gasto...`; | ||
| 94 | + } else { | ||
| 95 | + if (classifiers.length === 0) return 'Selecciona al menos un clasificador de ingreso...'; | ||
| 96 | + return `Buscar en ${classifiers.length} clasificador${classifiers.length > 1 ? 'es' : ''} de ingreso...`; | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + // Debounce | ||
| 101 | + let debounceTimer; | ||
| 102 | + let hintTimer; | ||
| 103 | + let showMinLengthHint = false; | ||
| 104 | + const DEBOUNCE_MS = 300; | ||
| 105 | + const HINT_DELAY_MS = 1500; | ||
| 106 | + | ||
| 107 | + const POBLACION = 12000000; | ||
| 108 | + const API_BASE = '/api/search'; | ||
| 109 | + | ||
| 110 | + // Hint diario | ||
| 111 | + let dailyData = null; | ||
| 112 | + let dailyLoading = true; | ||
| 113 | + | ||
| 114 | + async function fetchDaily() { | ||
| 115 | + try { | ||
| 116 | + const res = await fetch('/api/daily-summary'); | ||
| 117 | + if (res.ok) dailyData = await res.json(); | ||
| 118 | + } catch { | ||
| 119 | + // silencioso — es un hint, no crítico | ||
| 120 | + } finally { | ||
| 121 | + dailyLoading = false; | ||
| 122 | + } | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + // Ejemplos | ||
| 126 | + const EXAMPLES = [ | ||
| 127 | + { label: 'Vía férrea Bulo Bulo', color: 'rgba(201,167,81,0.12)', textColor: '#d4b962' }, | ||
| 128 | + { label: 'Viru Viru', color: 'rgba(150,155,175,0.10)', textColor: '#a0a5b8' }, | ||
| 129 | + { label: 'Cancha de fútbol', color: 'rgba(180,130,100,0.10)', textColor: '#b8947a' }, | ||
| 130 | + ]; | ||
| 131 | + | ||
| 132 | + // Navegación | ||
| 133 | + const SECTIONS_NAV = [ | ||
| 134 | + { id: 'hero', label: 'Inicio' }, | ||
| 135 | + { id: 'datos', label: 'Datos' }, | ||
| 136 | + { id: 'clasificadores', label: 'Clasificadores' }, | ||
| 137 | + { id: 'historias', label: 'Historias' }, | ||
| 138 | + { id: 'visualizaciones',label: 'Visualizaciones' }, | ||
| 139 | + ]; | ||
| 140 | + let currentSection = 'hero'; | ||
| 141 | + | ||
| 142 | + function scrollToSection(id) { | ||
| 143 | + document.querySelector(`[data-section="${id}"]`)?.scrollIntoView({ behavior: 'smooth' }); | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + onMount(() => { | ||
| 147 | + setTimeout(() => { mounted = true; }, 80); | ||
| 148 | + fetchDaily(); | ||
| 149 | + | ||
| 150 | + const isMobile = window.innerWidth < 768; | ||
| 151 | + if (!isMobile) setTimeout(() => searchInput?.focus(), 300); | ||
| 152 | + | ||
| 153 | + const sections = document.querySelectorAll('[data-section]'); | ||
| 154 | + const observer = new IntersectionObserver(entries => { | ||
| 155 | + entries.forEach(e => { if (e.isIntersecting) currentSection = e.target.dataset.section; }); | ||
| 156 | + }, { threshold: 0.5 }); | ||
| 157 | + sections.forEach(s => observer.observe(s)); | ||
| 158 | + | ||
| 159 | + return () => observer.disconnect(); | ||
| 160 | + }); | ||
| 161 | + | ||
| 162 | + function searchExample(label) { | ||
| 163 | + searchVal = label; | ||
| 164 | + performSearch(label); | ||
| 165 | + searchInput?.focus(); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + async function handleInput(e) { | ||
| 169 | + searchVal = e.target.value; | ||
| 170 | + clearTimeout(debounceTimer); | ||
| 171 | + clearTimeout(hintTimer); | ||
| 172 | + showMinLengthHint = false; | ||
| 173 | + | ||
| 174 | + if (searchVal.length < 3) { | ||
| 175 | + results = null; | ||
| 176 | + error = null; | ||
| 177 | + if (searchVal.length > 0) { | ||
| 178 | + hintTimer = setTimeout(() => { showMinLengthHint = true; }, HINT_DELAY_MS); | ||
| 179 | + } | ||
| 180 | + return; | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + const needsClassifiers = searchMode === 'ingresos' || | ||
| 184 | + (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 185 | + if (needsClassifiers && selectedClassifiers.length === 0) { | ||
| 186 | + results = null; | ||
| 187 | + error = null; | ||
| 188 | + return; | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + debounceTimer = setTimeout(() => performSearch(searchVal), DEBOUNCE_MS); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + async function performSearch(query, page = 1) { | ||
| 195 | + if (page === 1) { | ||
| 196 | + isLoading = true; | ||
| 197 | + allHits = []; | ||
| 198 | + sortBy = 'amount'; | ||
| 199 | + sortOrder = 'desc'; | ||
| 200 | + } else { | ||
| 201 | + isLoadingMore = true; | ||
| 202 | + } | ||
| 203 | + error = null; | ||
| 204 | + currentPage = page; | ||
| 205 | + | ||
| 206 | + try { | ||
| 207 | + const isClassSearch = searchMode === 'ingresos' || | ||
| 208 | + (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 209 | + const params = new URLSearchParams({ | ||
| 210 | + q: query, | ||
| 211 | + is_class: isClassSearch ? 'true' : 'false', | ||
| 212 | + mode: searchMode, | ||
| 213 | + page: page.toString() | ||
| 214 | + }); | ||
| 215 | + if (isClassSearch && selectedClassifiers.length > 0) { | ||
| 216 | + params.set('class_', selectedClassifiers.join(',')); | ||
| 217 | + } | ||
| 218 | + | ||
| 219 | + const res = await fetch(`${API_BASE}?${params}`); | ||
| 220 | + if (!res.ok) throw new Error(`Error ${res.status}`); | ||
| 221 | + const data = await res.json(); | ||
| 222 | + results = data; | ||
| 223 | + allHits = page === 1 ? data.hits : [...allHits, ...data.hits]; | ||
| 224 | + } catch (err) { | ||
| 225 | + console.error(err); | ||
| 226 | + error = 'Error al buscar. Intenta de nuevo.'; | ||
| 227 | + if (page === 1) { results = null; allHits = []; } | ||
| 228 | + } | ||
| 229 | + | ||
| 230 | + isLoading = false; | ||
| 231 | + isLoadingMore = false; | ||
| 232 | + } | ||
| 233 | + | ||
| 234 | + function loadMore() { | ||
| 235 | + if (hasMoreResults && !isLoadingMore) performSearch(searchVal, currentPage + 1); | ||
| 236 | + } | ||
| 237 | + | ||
| 238 | + function handleKeydown(e) { | ||
| 239 | + if (e.key === 'Escape') { clearSearch(); searchInput?.blur(); } | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + function clearSearch() { | ||
| 243 | + searchVal = ''; | ||
| 244 | + results = null; | ||
| 245 | + allHits = []; | ||
| 246 | + currentPage = 1; | ||
| 247 | + error = null; | ||
| 248 | + showMinLengthHint = false; | ||
| 249 | + clearTimeout(debounceTimer); | ||
| 250 | + clearTimeout(hintTimer); | ||
| 251 | + searchInput?.focus(); | ||
| 252 | + } | ||
| 253 | + | ||
| 254 | + // Helpers | ||
| 255 | + function extractAfterHyphen(text) { | ||
| 256 | + if (!text) return ''; | ||
| 257 | + const parts = text.split(' - '); | ||
| 258 | + return parts.length > 1 ? parts.slice(1).join(' - ').trim() : text; | ||
| 259 | + } | ||
| 260 | + | ||
| 261 | + function parseMetadatos(meta) { | ||
| 262 | + if (!meta) return {}; | ||
| 263 | + if (typeof meta === 'object') return meta; | ||
| 264 | + try { return JSON.parse(meta); } catch { return {}; } | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + function isClassifierSearch(data) { return data?.hits?.[0]?.document?.is_class === true; } | ||
| 268 | + function isEntidadSearch(data) { return data?.hits?.[0]?.document?.class_ === 'entidad'; } | ||
| 269 | + | ||
| 270 | + function getStats(data) { | ||
| 271 | + if (!data?.hits?.length) return null; | ||
| 272 | + const hits = data.hits; | ||
| 273 | + const isClassifier = isClassifierSearch(data); | ||
| 274 | + const montoTotal = hits.reduce((s, h) => s + (h.document.devengado || 0), 0); | ||
| 275 | + | ||
| 276 | + if (isClassifier) { | ||
| 277 | + const isEntidad = isEntidadSearch(data); | ||
| 278 | + const entidadesSet = new Set(); | ||
| 279 | + const subareasCount = {}; | ||
| 280 | + hits.forEach(h => { | ||
| 281 | + const m = parseMetadatos(h.document.metadatos); | ||
| 282 | + if (m.entidad) entidadesSet.add(m.entidad); | ||
| 283 | + if (isEntidad) { | ||
| 284 | + const sub = extractAfterHyphen(m.entidad_desc_subarea); | ||
| 285 | + if (sub) subareasCount[sub] = (subareasCount[sub] || 0) + 1; | ||
| 286 | + } | ||
| 287 | + }); | ||
| 288 | + return { found: data.found, montoTotal, perCapita: montoTotal / POBLACION, | ||
| 289 | + numEntidades: entidadesSet.size, subareasCount: isEntidad ? subareasCount : null, | ||
| 290 | + isClassifier: true, classType: hits[0]?.document?.class_ || null }; | ||
| 291 | + } else { | ||
| 292 | + const entidadesSet = new Set(); | ||
| 293 | + hits.forEach(h => { const m = parseMetadatos(h.document.metadatos); if (m.entidad) entidadesSet.add(m.entidad); }); | ||
| 294 | + const years = hits.map(h => h.document.gestion).filter(Boolean); | ||
| 295 | + const minYear = Math.min(...years); | ||
| 296 | + const maxYear = Math.max(...years); | ||
| 297 | + return { found: data.found, montoTotal, perCapita: montoTotal / POBLACION, | ||
| 298 | + numEntidades: entidadesSet.size, | ||
| 299 | + yearRange: minYear === maxYear ? `${minYear}` : `${minYear} – ${maxYear}`, | ||
| 300 | + isClassifier: false }; | ||
| 301 | + } | ||
| 302 | + } | ||
| 303 | + | ||
| 304 | + function formatMonto(num) { | ||
| 305 | + if (num >= 1e9) return `${(num/1e9).toFixed(1)} mil millones de Bs`; | ||
| 306 | + if (num >= 1e6) return `${(num/1e6).toFixed(1)} millones de Bs`; | ||
| 307 | + if (num >= 1e3) return `${(num/1e3).toFixed(0)} mil Bs`; | ||
| 308 | + return `${num.toFixed(0)} Bs`; | ||
| 309 | + } | ||
| 310 | + | ||
| 311 | + function formatMontoFull(num) { | ||
| 312 | + return `${num.toLocaleString('es-BO', { minimumFractionDigits: 0, maximumFractionDigits: 0 })} Bs`; | ||
| 313 | + } | ||
| 314 | + | ||
| 315 | + function generateCodigo(hit) { | ||
| 316 | + const m = parseMetadatos(hit.document.metadatos); | ||
| 317 | + return `${m.entidad}-${m.programa}-${m.proyecto}-${m.actividad}-${hit.document.gestion}`; | ||
| 318 | + } | ||
| 319 | + | ||
| 320 | + $: stats = results ? getStats(results) : null; | ||
| 321 | + | ||
| 322 | + $: { | ||
| 323 | + const _mode = searchMode; | ||
| 324 | + const _submode = searchSubmode; | ||
| 325 | + const _classifiers = selectedClassifiers; | ||
| 326 | + if (searchVal.length >= 3 && mounted) { | ||
| 327 | + clearTimeout(debounceTimer); | ||
| 328 | + const needsClassifiers = _mode === 'ingresos' || (_mode === 'gastos' && _submode === 'clasificadores'); | ||
| 329 | + if (!needsClassifiers || _classifiers.length > 0) { | ||
| 330 | + debounceTimer = setTimeout(() => performSearch(searchVal), 150); | ||
| 331 | + } else { | ||
| 332 | + results = null; | ||
| 333 | + } | ||
| 334 | + } | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + // ¿Mostrar chips de clasificadores? | ||
| 338 | + $: showClassifierChips = searchMode === 'ingresos' || | ||
| 339 | + (searchMode === 'gastos' && searchSubmode === 'clasificadores'); | ||
| 340 | + | ||
| 341 | + // ¿Hay búsqueda activa? | ||
| 342 | + $: isSearching = searchVal.length >= 3; | ||
| 343 | +</script> | ||
| 344 | + | ||
| 345 | +<svelte:head> | ||
| 346 | + <title>Buscador | Presupuesto Abierto</title> | ||
| 347 | +</svelte:head> | ||
| 348 | + | ||
| 349 | +<div class="page" class:mounted> | ||
| 350 | + | ||
| 351 | + <!-- Dots de navegación --> | ||
| 352 | + <nav class="section-dots"> | ||
| 353 | + {#each SECTIONS_NAV as s} | ||
| 354 | + <button | ||
| 355 | + class="section-dot" | ||
| 356 | + class:section-dot-active={currentSection === s.id} | ||
| 357 | + on:click={() => scrollToSection(s.id)} | ||
| 358 | + aria-label={s.label} | ||
| 359 | + > | ||
| 360 | + <span class="section-dot-label">{s.label}</span> | ||
| 361 | + </button> | ||
| 362 | + {/each} | ||
| 363 | + </nav> | ||
| 364 | + | ||
| 365 | + <!-- Hero --> | ||
| 366 | + <main data-section="hero"> | ||
| 367 | + | ||
| 368 | + <div class="nav-logo"> | ||
| 369 | + <img src="/logos_oscuro/05_Imago MEFP horizontal espacio negativo.png" alt="MEFP" /> | ||
| 370 | + </div> | ||
| 371 | + | ||
| 372 | + <button class="nav-menu-btn" aria-label="Menú"> | ||
| 373 | + <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> | ||
| 374 | + <line x1="3" y1="6" x2="21" y2="6"/> | ||
| 375 | + <line x1="3" y1="12" x2="21" y2="12"/> | ||
| 376 | + <line x1="3" y1="18" x2="21" y2="18"/> | ||
| 377 | + </svg> | ||
| 378 | + </button> | ||
| 379 | + | ||
| 380 | + <div class="search-container anim" style="--d:200ms"> | ||
| 381 | + | ||
| 382 | + <!-- Título --> | ||
| 383 | + <h1 class="hero-title" class:hero-collapsed={allHits.length > 0}> | ||
| 384 | + Presupuesto <span class="gold">Abierto</span> | ||
| 385 | + </h1> | ||
| 386 | + <p class="hero-sub" class:hero-collapsed={allHits.length > 0}> | ||
| 387 | + <a href="/vista-diaria" class="hero-sub-link">Información diaria</a> e histórica de todo el Estado Boliviano. | ||
| 388 | + </p> | ||
| 389 | + | ||
| 390 | + <!-- Buscador --> | ||
| 391 | + <div class="search-wrap"> | ||
| 392 | + <div class="search-bar" class:search-focused={searchFocused} class:has-results={isSearching}> | ||
| 393 | + <svg class="search-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" | ||
| 394 | + stroke={searchFocused ? '#D4B254' : '#8C8880'} stroke-width="2" stroke-linecap="round"> | ||
| 395 | + <circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/> | ||
| 396 | + </svg> | ||
| 397 | + | ||
| 398 | + <div class="search-input-wrap"> | ||
| 399 | + <input | ||
| 400 | + type="text" | ||
| 401 | + bind:this={searchInput} | ||
| 402 | + bind:value={searchVal} | ||
| 403 | + on:input={handleInput} | ||
| 404 | + on:keydown={handleKeydown} | ||
| 405 | + on:focus={() => searchFocused = true} | ||
| 406 | + on:blur={() => searchFocused = false} | ||
| 407 | + placeholder="" | ||
| 408 | + /> | ||
| 409 | + {#if !searchVal} | ||
| 410 | + <div class="search-placeholder"> | ||
| 411 | + <span class="blink-cursor" class:cursor-hidden={searchFocused}></span> | ||
| 412 | + <span class="placeholder-text">{placeholderText}</span> | ||
| 413 | + </div> | ||
| 414 | + {/if} | ||
| 415 | + {#if searchVal && !isLoading} | ||
| 416 | + <button class="search-clear" on:click={clearSearch}> | ||
| 417 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> | ||
| 418 | + <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> | ||
| 419 | + </svg> | ||
| 420 | + </button> | ||
| 421 | + {/if} | ||
| 422 | + {#if isLoading} | ||
| 423 | + <div class="search-loading"><span class="loading-dot"></span></div> | ||
| 424 | + {/if} | ||
| 425 | + </div> | ||
| 426 | + </div> | ||
| 427 | + | ||
| 428 | + <!-- ─── FILTROS INLINE ─── --> | ||
| 429 | + <div class="filters-row" class:filters-hidden={isSearching}> | ||
| 430 | + | ||
| 431 | + <!-- Fila 1: Gastos / Ingresos --> | ||
| 432 | + <div class="filter-group"> | ||
| 433 | + <button | ||
| 434 | + class="filter-chip filter-chip-mode" | ||
| 435 | + class:filter-chip-active={searchMode === 'gastos'} | ||
| 436 | + on:click={() => setMode('gastos')} | ||
| 437 | + > | ||
| 438 | + Gastos | ||
| 439 | + </button> | ||
| 440 | + <button | ||
| 441 | + class="filter-chip filter-chip-mode" | ||
| 442 | + class:filter-chip-active={searchMode === 'ingresos'} | ||
| 443 | + on:click={() => setMode('ingresos')} | ||
| 444 | + > | ||
| 445 | + Ingresos | ||
| 446 | + </button> | ||
| 447 | + | ||
| 448 | + <span class="filter-divider"></span> | ||
| 449 | + | ||
| 450 | + <!-- Sub-modo (solo cuando Gastos está activo) --> | ||
| 451 | + {#if searchMode === 'gastos'} | ||
| 452 | + <button | ||
| 453 | + class="filter-chip" | ||
| 454 | + class:filter-chip-active={searchSubmode === 'programas'} | ||
| 455 | + on:click={() => setSubmode('programas')} | ||
| 456 | + > | ||
| 457 | + Programas y proyectos | ||
| 458 | + </button> | ||
| 459 | + <button | ||
| 460 | + class="filter-chip" | ||
| 461 | + class:filter-chip-active={searchSubmode === 'clasificadores'} | ||
| 462 | + on:click={() => setSubmode('clasificadores')} | ||
| 463 | + > | ||
| 464 | + Clasificadores | ||
| 465 | + </button> | ||
| 466 | + {/if} | ||
| 467 | + </div> | ||
| 468 | + | ||
| 469 | + <!-- Fila 2: Clasificadores (cuando aplica) --> | ||
| 470 | + {#if showClassifierChips} | ||
| 471 | + <div class="filter-group filter-group-classifiers"> | ||
| 472 | + {#each activeClasificadores as clf} | ||
| 473 | + <button | ||
| 474 | + class="filter-chip filter-chip-classifier" | ||
| 475 | + class:filter-chip-active={selectedClassifiers.includes(clf.id)} | ||
| 476 | + class:filter-chip-disabled={clf.disabled} | ||
| 477 | + on:click={() => { if (!clf.disabled) toggleClassifier(clf.id); }} | ||
| 478 | + disabled={clf.disabled} | ||
| 479 | + > | ||
| 480 | + {clf.label} | ||
| 481 | + {#if clf.disabled} | ||
| 482 | + <span class="chip-soon">próx.</span> | ||
| 483 | + {/if} | ||
| 484 | + </button> | ||
| 485 | + {/each} | ||
| 486 | + </div> | ||
| 487 | + {/if} | ||
| 488 | + </div> | ||
| 489 | + | ||
| 490 | + <!-- ─── ÁREA DE RESULTADOS / EJEMPLOS / HINT ─── --> | ||
| 491 | + <div class="results-area"> | ||
| 492 | + | ||
| 493 | + <!-- Ejemplos --> | ||
| 494 | + <div class="examples" class:examples-hidden={isSearching}> | ||
| 495 | + <span class="examples-label">Prueba con:</span> | ||
| 496 | + {#each EXAMPLES as ex} | ||
| 497 | + <button | ||
| 498 | + class="example-chip" | ||
| 499 | + style="background:{ex.color}; color:{ex.textColor}" | ||
| 500 | + on:click={() => searchExample(ex.label)} | ||
| 501 | + > | ||
| 502 | + {ex.label} | ||
| 503 | + </button> | ||
| 504 | + {/each} | ||
| 505 | + </div> | ||
| 506 | + | ||
| 507 | + <!-- Hint diario --> | ||
| 508 | + {#if !isSearching && !dailyLoading} | ||
| 509 | + <a | ||
| 510 | + href="/vista-diaria" | ||
| 511 | + class="daily-hint" | ||
| 512 | + class:daily-hint-visible={!isSearching} | ||
| 513 | + > | ||
| 514 | + <span class="daily-dot"></span> | ||
| 515 | + <span class="daily-text"> | ||
| 516 | + {#if dailyData} | ||
| 517 | + Ayer el Estado ejecutó <strong>{formatMonto(dailyData.monto)}</strong> | ||
| 518 | + {:else} | ||
| 519 | + Ver el resumen de ejecución de ayer | ||
| 520 | + {/if} | ||
| 521 | + </span> | ||
| 522 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 523 | + <path d="M5 12h14M12 5l7 7-7 7"/> | ||
| 524 | + </svg> | ||
| 525 | + </a> | ||
| 526 | + {/if} | ||
| 527 | + | ||
| 528 | + <!-- Estados de búsqueda --> | ||
| 529 | + {#if showMinLengthHint && searchVal.length > 0 && searchVal.length < 3} | ||
| 530 | + <div class="results-content"> | ||
| 531 | + <div class="results-hint">Escribe al menos 3 letras para buscar</div> | ||
| 532 | + </div> | ||
| 533 | + {:else if error && !results} | ||
| 534 | + <div class="results-content"> | ||
| 535 | + <div class="results-error">{error}</div> | ||
| 536 | + </div> | ||
| 537 | + {:else if isLoading && !results} | ||
| 538 | + <div class="results-content"> | ||
| 539 | + <div class="results-loading">Buscando...</div> | ||
| 540 | + </div> | ||
| 541 | + {:else if allHits.length > 0} | ||
| 542 | + <div class="results-content results-content-filled"> | ||
| 543 | + | ||
| 544 | + {#if stats} | ||
| 545 | + <div class="results-summary"> | ||
| 546 | + <div class="summary-row"> | ||
| 547 | + <div class="summary-left"> | ||
| 548 | + <div class="summary-header"> | ||
| 549 | + <span class="summary-count">{sortedHits.length}</span> | ||
| 550 | + <span class="summary-header-text">de {stats.found.toLocaleString()} resultados</span> | ||
| 551 | + {#if isLoading}<span class="updating-dot"></span>{/if} | ||
| 552 | + </div> | ||
| 553 | + <div class="summary-meta"> | ||
| 554 | + <span class="summary-tag summary-tag-gold"> | ||
| 555 | + {formatMonto(stats.montoTotal)} <span class="summary-period">· 2006–2025</span> | ||
| 556 | + </span> | ||
| 557 | + {#if stats.isClassifier} | ||
| 558 | + {#if stats.subareasCount} | ||
| 559 | + <span class="summary-tag summary-tag-entities">{stats.numEntidades} {stats.numEntidades === 1 ? 'entidad' : 'entidades'}</span> | ||
| 560 | + <span class="summary-arrow">→</span> | ||
| 561 | + {#each Object.entries(stats.subareasCount).sort((a,b) => b[1]-a[1]) as [sub, count]} | ||
| 562 | + <span class="summary-tag summary-tag-subarea">{sub}: {count}</span> | ||
| 563 | + {/each} | ||
| 564 | + {:else} | ||
| 565 | + <span class="summary-tag">{stats.found} resultados en {stats.classType}</span> | ||
| 566 | + {/if} | ||
| 567 | + {:else} | ||
| 568 | + <span class="summary-tag">{stats.numEntidades} {stats.numEntidades === 1 ? 'institución' : 'instituciones'}</span> | ||
| 569 | + {#if stats.yearRange} | ||
| 570 | + <span class="summary-tag">{stats.yearRange}</span> | ||
| 571 | + {/if} | ||
| 572 | + {/if} | ||
| 573 | + </div> | ||
| 574 | + </div> | ||
| 575 | + <div class="sort-buttons"> | ||
| 576 | + {#if !stats.isClassifier} | ||
| 577 | + <button class="sort-btn" class:sort-btn-active={sortBy === 'year'} on:click={() => toggleSort('year')}> | ||
| 578 | + Año | ||
| 579 | + {#if sortBy === 'year'} | ||
| 580 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 581 | + {#if sortOrder === 'desc'}<path d="M12 5v14M5 12l7 7 7-7"/> | ||
| 582 | + {:else}<path d="M12 19V5M5 12l7-7 7 7"/>{/if} | ||
| 583 | + </svg> | ||
| 584 | + {/if} | ||
| 585 | + </button> | ||
| 586 | + {/if} | ||
| 587 | + <button class="sort-btn" class:sort-btn-active={sortBy === 'amount'} on:click={() => toggleSort('amount')}> | ||
| 588 | + Monto | ||
| 589 | + {#if sortBy === 'amount'} | ||
| 590 | + <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 591 | + {#if sortOrder === 'desc'}<path d="M12 5v14M5 12l7 7 7-7"/> | ||
| 592 | + {:else}<path d="M12 19V5M5 12l7-7 7 7"/>{/if} | ||
| 593 | + </svg> | ||
| 594 | + {/if} | ||
| 595 | + </button> | ||
| 596 | + </div> | ||
| 597 | + </div> | ||
| 598 | + </div> | ||
| 599 | + {/if} | ||
| 600 | + | ||
| 601 | + <div class="results-list"> | ||
| 602 | + {#each sortedHits as hit, i} | ||
| 603 | + {@const meta = parseMetadatos(hit.document.metadatos)} | ||
| 604 | + {@const isEntidadClass = hit.document.class_ === 'entidad'} | ||
| 605 | + {@const isDA = isEntidadClass && meta.da && meta.entidad_desc_entidad} | ||
| 606 | + {@const subarea = isEntidadClass ? extractAfterHyphen(meta.entidad_desc_subarea) : null} | ||
| 607 | + {@const parentEntity = isDA ? extractAfterHyphen(meta.entidad_desc_entidad) : null} | ||
| 608 | + {@const isClassResult = hit.document.is_class === true} | ||
| 609 | + <a | ||
| 610 | + href={isEntidadClass | ||
| 611 | + ? `/entidad/${meta.entidad}` | ||
| 612 | + : (isClassResult ? `/clasificador/${hit.document.class_}/${hit.document.id}` : `/proyecto/${generateCodigo(hit)}`)} | ||
| 613 | + class="result-card" | ||
| 614 | + style="--delay: {i * 30}ms" | ||
| 615 | + > | ||
| 616 | + <div class="result-content"> | ||
| 617 | + <div class="result-text"> | ||
| 618 | + {#if hit.highlights?.[0]?.snippet} | ||
| 619 | + {@html hit.highlights[0].snippet} | ||
| 620 | + {:else} | ||
| 621 | + {hit.document.texto} | ||
| 622 | + {/if} | ||
| 623 | + </div> | ||
| 624 | + <div class="result-meta"> | ||
| 625 | + {#if isEntidadClass} | ||
| 626 | + {#if isDA}<span class="result-da-badge">DA</span><span class="result-parent">de {parentEntity}</span>{/if} | ||
| 627 | + {#if subarea}<span class="result-subarea">{subarea}</span>{/if} | ||
| 628 | + {:else if isClassResult} | ||
| 629 | + <span class="result-class-type">{hit.document.class_}</span> | ||
| 630 | + {:else} | ||
| 631 | + <span class="result-year">{hit.document.gestion}</span> | ||
| 632 | + {/if} | ||
| 633 | + <span class="result-monto">{formatMontoFull(hit.document.devengado)}</span> | ||
| 634 | + </div> | ||
| 635 | + </div> | ||
| 636 | + <svg class="result-link-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | ||
| 637 | + <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/> | ||
| 638 | + <polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/> | ||
| 639 | + </svg> | ||
| 640 | + </a> | ||
| 641 | + {/each} | ||
| 642 | + </div> | ||
| 643 | + | ||
| 644 | + {#if hasMoreResults} | ||
| 645 | + <div class="load-more-container"> | ||
| 646 | + <button class="load-more-btn" on:click={loadMore} disabled={isLoadingMore}> | ||
| 647 | + {#if isLoadingMore} | ||
| 648 | + <span class="loading-dot"></span> Cargando... | ||
| 649 | + {:else} | ||
| 650 | + Cargar más ({allHits.length} de {results.found}) | ||
| 651 | + {/if} | ||
| 652 | + </button> | ||
| 653 | + </div> | ||
| 654 | + {/if} | ||
| 655 | + </div> | ||
| 656 | + | ||
| 657 | + {:else if results && results.hits?.length === 0} | ||
| 658 | + <div class="results-content"> | ||
| 659 | + <div class="results-empty">Sin resultados para "{searchVal}"</div> | ||
| 660 | + </div> | ||
| 661 | + {/if} | ||
| 662 | + | ||
| 663 | + <div class="area-spacer" class:area-spacer-hidden={allHits.length > 0}></div> | ||
| 664 | + </div> | ||
| 665 | + </div> | ||
| 666 | + </div> | ||
| 667 | + | ||
| 668 | + <!-- Scroll hint --> | ||
| 669 | + <div class="scroll-hint anim" style="--d:800ms" class:scroll-hint-hidden={isSearching}> | ||
| 670 | + <svg class="scroll-icon-mouse" width="18" height="28" viewBox="0 0 18 28" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"> | ||
| 671 | + <rect x="1" y="1" width="16" height="26" rx="8"/><line x1="9" y1="7" x2="9" y2="12" opacity="0.7"/> | ||
| 672 | + </svg> | ||
| 673 | + <svg class="scroll-icon-touch" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | ||
| 674 | + <path d="M6 9l6 6 6-6"/> | ||
| 675 | + </svg> | ||
| 676 | + <span>Explora la página</span> | ||
| 677 | + </div> | ||
| 678 | + | ||
| 679 | + </main> | ||
| 680 | +</div> | ||
| 681 | + | ||
| 682 | +<style> | ||
| 683 | + @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&display=swap'); | ||
| 684 | + | ||
| 685 | + :global(*) { margin: 0; padding: 0; box-sizing: border-box; } | ||
| 686 | + :global(html), :global(body) { overflow-x: hidden; } | ||
| 687 | + | ||
| 688 | + .page { | ||
| 689 | + --serif: 'Qanelas', system-ui, sans-serif; | ||
| 690 | + --sans: 'Qanelas', system-ui, sans-serif; | ||
| 691 | + --mono: 'JetBrains Mono', monospace; | ||
| 692 | + --gold: #D4B254; | ||
| 693 | + --gold-bright: #EAD06A; | ||
| 694 | + --gold-soft: rgba(212,178,84,0.15); | ||
| 695 | + --green: #4A8B6E; | ||
| 696 | + --bg: #1A1A18; | ||
| 697 | + --bg-elevated: #232321; | ||
| 698 | + --bg-surface: #2A2A28; | ||
| 699 | + --text: #F7F3EB; | ||
| 700 | + --text-secondary: #E0DCD4; | ||
| 701 | + --text-muted: #B5B1A9; | ||
| 702 | + --text-dim: #8C8880; | ||
| 703 | + | ||
| 704 | + font-family: var(--sans); | ||
| 705 | + background: var(--bg); | ||
| 706 | + color: var(--text); | ||
| 707 | + height: 100vh; | ||
| 708 | + max-height: 100vh; | ||
| 709 | + overflow: hidden; | ||
| 710 | + display: flex; | ||
| 711 | + flex-direction: column; | ||
| 712 | + -webkit-font-smoothing: antialiased; | ||
| 713 | + -moz-osx-font-smoothing: grayscale; | ||
| 714 | + text-rendering: optimizeLegibility; | ||
| 715 | + } | ||
| 716 | + | ||
| 717 | + @keyframes pulse { 0%,100% { opacity:0.4; } 50% { opacity:1; } } | ||
| 718 | + @keyframes blink { 0%,100% { opacity:1; } 50% { opacity:0; } } | ||
| 719 | + | ||
| 720 | + /* ── Dots ── */ | ||
| 721 | + .section-dots { | ||
| 722 | + position: fixed; right: 24px; top: 50%; transform: translateY(-50%); | ||
| 723 | + z-index: 100; display: flex; flex-direction: column; gap: 12px; | ||
| 724 | + background: rgba(255,255,255,0.06); backdrop-filter: blur(8px); | ||
| 725 | + padding: 12px 8px; border-radius: 20px; | ||
| 726 | + } | ||
| 727 | + .section-dot { | ||
| 728 | + width:10px; height:10px; border-radius:50%; | ||
| 729 | + border:1.5px solid rgba(180,177,169,0.4); | ||
| 730 | + background:rgba(180,177,169,0.15); | ||
| 731 | + cursor:pointer; transition:all 0.3s; padding:0; position:relative; | ||
| 732 | + } | ||
| 733 | + .section-dot:hover { border-color:rgba(180,177,169,0.6); background:rgba(180,177,169,0.3); transform:scale(1.2); } | ||
| 734 | + .section-dot-active { background:var(--gold); border-color:var(--gold); } | ||
| 735 | + .section-dot-label { | ||
| 736 | + position:absolute; right:20px; top:50%; transform:translateY(-50%); | ||
| 737 | + font-family:var(--mono); font-size:10px; letter-spacing:0.05em; | ||
| 738 | + color:var(--text-dim); white-space:nowrap; opacity:0; transition:opacity 0.2s; pointer-events:none; | ||
| 739 | + } | ||
| 740 | + .section-dot:hover .section-dot-label { opacity:1; } | ||
| 741 | + | ||
| 742 | + /* ── Anim ── */ | ||
| 743 | + .anim { opacity:0; transform:translateY(18px); transition:all 0.8s cubic-bezier(0.16,1,0.3,1); transition-delay:var(--d,0ms); } | ||
| 744 | + .mounted .anim { opacity:1; transform:translateY(0); } | ||
| 745 | + | ||
| 746 | + /* ── Nav ── */ | ||
| 747 | + .nav-logo { position:absolute; top:24px; left:32px; z-index:50; opacity:0.75; pointer-events:none; } | ||
| 748 | + .nav-logo img { height:100px; width:auto; object-fit:contain; } | ||
| 749 | + .nav-menu-btn { | ||
| 750 | + position:absolute; top:24px; right:24px; z-index:50; | ||
| 751 | + background:none; border:none; cursor:pointer; color:var(--text-dim); | ||
| 752 | + opacity:0.6; padding:8px; border-radius:8px; transition:all 0.2s; | ||
| 753 | + } | ||
| 754 | + .nav-menu-btn:hover { opacity:1; color:var(--text); background:rgba(255,255,255,0.05); } | ||
| 755 | + | ||
| 756 | + /* ── Main ── */ | ||
| 757 | + main { | ||
| 758 | + flex:1; display:flex; flex-direction:column; align-items:center; | ||
| 759 | + justify-content:flex-start; padding:28vh 48px 0; | ||
| 760 | + position:relative; overflow:hidden; | ||
| 761 | + } | ||
| 762 | + | ||
| 763 | + .search-container { width:100%; max-width:820px; text-align:center; position:relative; } | ||
| 764 | + | ||
| 765 | + /* ── Título ── */ | ||
| 766 | + .hero-title { | ||
| 767 | + font-size:clamp(40px,5.5vw,64px); font-weight:900; | ||
| 768 | + line-height:1.05; letter-spacing:-0.035em; color:var(--text); | ||
| 769 | + margin-top:40px; transition:all 0.35s cubic-bezier(0.16,1,0.3,1); | ||
| 770 | + } | ||
| 771 | + .hero-title.hero-collapsed { opacity:0; height:0; overflow:hidden; margin:0; } | ||
| 772 | + | ||
| 773 | + .hero-sub { | ||
| 774 | + font-size:clamp(16px,1.6vw,18px); color:#A5A29A; | ||
| 775 | + margin-top:10px; margin-bottom:16px; line-height:1.6; | ||
| 776 | + transition:all 0.35s cubic-bezier(0.16,1,0.3,1); | ||
| 777 | + } | ||
| 778 | + .hero-sub.hero-collapsed { opacity:0; height:0; overflow:hidden; margin:0; } | ||
| 779 | + | ||
| 780 | + .hero-sub-link { color:inherit; text-decoration:none; border-bottom:1px dotted rgba(165,162,154,0.5); transition:all 0.2s; padding-bottom:1px; } | ||
| 781 | + .hero-sub-link:hover { color:#4A8B6E; border-bottom-color:#4A8B6E; } | ||
| 782 | + .gold { color:var(--gold); } | ||
| 783 | + | ||
| 784 | + /* ── Search bar ── */ | ||
| 785 | + .search-wrap { position:relative; border-radius:16px; transition:all 0.35s cubic-bezier(0.16,1,0.3,1); z-index:20; } | ||
| 786 | + .search-bar { | ||
| 787 | + display:flex; align-items:center; background:var(--bg-elevated); | ||
| 788 | + border-radius:16px; padding:8px 20px; | ||
| 789 | + border:1px solid transparent; transition:all 0.3s cubic-bezier(0.16,1,0.3,1); | ||
| 790 | + } | ||
| 791 | + .search-focused { background:var(--bg-surface); border:1px solid rgba(255,255,255,0.08); box-shadow:0 12px 40px rgba(0,0,0,0.25); } | ||
| 792 | + .search-bar.has-results { border-radius:16px 16px 0 0; background:var(--bg-surface); border:1px solid rgba(255,255,255,0.08); border-bottom:none; box-shadow:0 12px 40px rgba(0,0,0,0.25); } | ||
| 793 | + .search-icon { flex-shrink:0; transition:stroke 0.3s; } | ||
| 794 | + .search-input-wrap { flex:1; position:relative; display:flex; align-items:center; } | ||
| 795 | + .search-input-wrap input { | ||
| 796 | + width:100%; background:transparent; border:none; outline:none; | ||
| 797 | + color:var(--text); font-size:17px; font-weight:500; font-family:var(--sans); | ||
| 798 | + padding:14px 16px 14px 10px; caret-color:var(--gold); letter-spacing:-0.01em; | ||
| 799 | + } | ||
| 800 | + .search-placeholder { position:absolute; left:10px; top:50%; transform:translateY(-50%); display:flex; align-items:center; pointer-events:none; } | ||
| 801 | + .blink-cursor { width:2px; height:20px; background:var(--gold); margin-right:4px; animation:blink 1s step-end infinite; border-radius:1px; flex-shrink:0; } | ||
| 802 | + .blink-cursor.cursor-hidden { opacity:0; animation:none; } | ||
| 803 | + .placeholder-text { font-family:var(--sans); font-size:16px; color:#B8B5AD; } | ||
| 804 | + .search-clear { background:rgba(255,255,255,0.08); border:none; border-radius:8px; width:32px; height:32px; display:flex; align-items:center; justify-content:center; cursor:pointer; color:var(--text-muted); transition:all 0.2s; } | ||
| 805 | + .search-clear:hover { background:rgba(255,255,255,0.15); color:var(--text); } | ||
| 806 | + .search-loading { display:flex; align-items:center; padding-right:8px; } | ||
| 807 | + .loading-dot { width:8px; height:8px; border-radius:50%; background:var(--gold); animation:pulse 1s ease-in-out infinite; } | ||
| 808 | + | ||
| 809 | + /* ───────────────────────────────────────── | ||
| 810 | + FILTROS INLINE — el cambio principal | ||
| 811 | + ───────────────────────────────────────── */ | ||
| 812 | + .filters-row { | ||
| 813 | + display:flex; flex-direction:column; gap:8px; | ||
| 814 | + padding:10px 2px 0; | ||
| 815 | + transition:opacity 0.25s ease, max-height 0.35s cubic-bezier(0.16,1,0.3,1); | ||
| 816 | + max-height:120px; overflow:hidden; | ||
| 817 | + } | ||
| 818 | + .filters-hidden { opacity:0; max-height:0; pointer-events:none; } | ||
| 819 | + | ||
| 820 | + .filter-group { | ||
| 821 | + display:flex; align-items:center; gap:6px; flex-wrap:wrap; | ||
| 822 | + justify-content:center; | ||
| 823 | + } | ||
| 824 | + .filter-group-classifiers { padding-left:0; } | ||
| 825 | + | ||
| 826 | + .filter-divider { | ||
| 827 | + width:1px; height:14px; background:rgba(255,255,255,0.12); | ||
| 828 | + margin:0 4px; flex-shrink:0; | ||
| 829 | + } | ||
| 830 | + | ||
| 831 | + .filter-chip { | ||
| 832 | + font-family:var(--sans); font-size:12px; font-weight:500; | ||
| 833 | + padding:5px 12px; border-radius:20px; border:1px solid rgba(255,255,255,0.1); | ||
| 834 | + background:rgba(255,255,255,0.04); color:var(--text-muted); | ||
| 835 | + cursor:pointer; transition:all 0.18s; white-space:nowrap; | ||
| 836 | + display:flex; align-items:center; gap:5px; | ||
| 837 | + } | ||
| 838 | + .filter-chip:hover:not(:disabled) { | ||
| 839 | + background:rgba(255,255,255,0.08); color:var(--text); | ||
| 840 | + border-color:rgba(255,255,255,0.18); | ||
| 841 | + } | ||
| 842 | + .filter-chip-active { | ||
| 843 | + background:rgba(212,178,84,0.12); color:var(--gold); | ||
| 844 | + border-color:rgba(212,178,84,0.35); | ||
| 845 | + } | ||
| 846 | + .filter-chip-active:hover { background:rgba(212,178,84,0.18); } | ||
| 847 | + | ||
| 848 | + .filter-chip-mode { font-weight:600; font-size:12.5px; } | ||
| 849 | + | ||
| 850 | + .filter-chip-disabled { | ||
| 851 | + opacity:0.4; cursor:default; pointer-events:none; | ||
| 852 | + } | ||
| 853 | + | ||
| 854 | + .chip-soon { | ||
| 855 | + font-family:var(--mono); font-size:9px; letter-spacing:0.04em; | ||
| 856 | + color:var(--text-dim); background:rgba(255,255,255,0.06); | ||
| 857 | + padding:1px 5px; border-radius:4px; | ||
| 858 | + } | ||
| 859 | + | ||
| 860 | + /* ───────────────────────────────────────── | ||
| 861 | + HINT DIARIO | ||
| 862 | + ───────────────────────────────────────── */ | ||
| 863 | + .daily-hint { | ||
| 864 | + display:inline-flex; align-items:center; gap:8px; | ||
| 865 | + padding:8px 16px; border-radius:20px; | ||
| 866 | + border:1px solid rgba(74,139,110,0.25); | ||
| 867 | + background:rgba(74,139,110,0.07); | ||
| 868 | + color:var(--text-muted); text-decoration:none; | ||
| 869 | + font-size:13px; transition:all 0.2s; | ||
| 870 | + opacity:0; pointer-events:none; | ||
| 871 | + transform:translateY(4px); | ||
| 872 | + } | ||
| 873 | + .daily-hint-visible { opacity:1; pointer-events:auto; transform:translateY(0); transition:opacity 0.4s ease 0.1s, transform 0.4s ease 0.1s, background 0.2s, border-color 0.2s; } | ||
| 874 | + .daily-hint:hover { background:rgba(74,139,110,0.12); border-color:rgba(74,139,110,0.4); color:var(--text); } | ||
| 875 | + .daily-hint strong { color:#6BBF97; font-weight:600; } | ||
| 876 | + | ||
| 877 | + .daily-dot { | ||
| 878 | + width:7px; height:7px; border-radius:50%; | ||
| 879 | + background:#4A8B6E; flex-shrink:0; | ||
| 880 | + box-shadow:0 0 0 3px rgba(74,139,110,0.2); | ||
| 881 | + animation:pulse 2s ease-in-out infinite; | ||
| 882 | + } | ||
| 883 | + | ||
| 884 | + /* ── Results area ── */ | ||
| 885 | + .results-area { margin-top:16px; min-height:240px; position:relative; } | ||
| 886 | + .area-spacer { position:absolute; bottom:0; left:50%; transform:translateX(-50%); height:80px; pointer-events:none; transition:opacity 0.4s; } | ||
| 887 | + .area-spacer-hidden { opacity:0; } | ||
| 888 | + | ||
| 889 | + /* ── Examples ── */ | ||
| 890 | + .examples { | ||
| 891 | + display:flex; align-items:center; justify-content:center; | ||
| 892 | + gap:10px; flex-wrap:wrap; padding:12px 0 16px; | ||
| 893 | + transition:opacity 0.25s ease; | ||
| 894 | + } | ||
| 895 | + .examples-label { font-size:13px; color:var(--text-dim); } | ||
| 896 | + .example-chip { font-family:var(--sans); font-size:13px; padding:8px 14px; border-radius:20px; border:none; cursor:pointer; transition:all 0.2s; font-weight:500; } | ||
| 897 | + .example-chip:hover { transform:translateY(-1px); filter:brightness(1.1); } | ||
| 898 | + .examples-hidden { opacity:0; pointer-events:none; } | ||
| 899 | + | ||
| 900 | + /* ── Results ── */ | ||
| 901 | + .results-content { position:absolute; top:-16px; left:0; right:0; z-index:10; text-align:left; } | ||
| 902 | + .results-content-filled { | ||
| 903 | + background:var(--bg-surface); border-radius:0 0 16px 16px; | ||
| 904 | + border:1px solid rgba(255,255,255,0.08); border-top:none; | ||
| 905 | + box-shadow:0 12px 40px rgba(0,0,0,0.25); max-height:450px; overflow-y:auto; | ||
| 906 | + } | ||
| 907 | + .results-loading, .results-error, .results-empty, .results-hint { | ||
| 908 | + padding:16px 24px; text-align:center; color:var(--text-muted); font-size:14px; line-height:1.5; | ||
| 909 | + } | ||
| 910 | + .results-error { color:#F28B82; } | ||
| 911 | + .results-empty { color:var(--text-dim); } | ||
| 912 | + | ||
| 913 | + /* Summary */ | ||
| 914 | + .results-summary { padding:12px 16px; border-bottom:1px solid rgba(255,255,255,0.06); } | ||
| 915 | + .summary-row { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; } | ||
| 916 | + .summary-left { flex:1; min-width:0; } | ||
| 917 | + .summary-header { display:flex; align-items:baseline; gap:6px; margin-bottom:4px; flex-wrap:wrap; } | ||
| 918 | + .summary-count { font-weight:700; color:var(--text); font-size:20px; font-family:var(--mono); letter-spacing:-0.02em; } | ||
| 919 | + .summary-header-text { color:var(--text-secondary); font-size:15px; } | ||
| 920 | + .updating-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--gold); animation:pulse 0.8s ease-in-out infinite; margin-left:6px; } | ||
| 921 | + .summary-meta { display:flex; flex-wrap:wrap; gap:6px; align-items:center; } | ||
| 922 | + .summary-tag { font-family:var(--mono); font-size:12px; color:var(--text-dim); background:rgba(255,255,255,0.05); padding:3px 8px; border-radius:4px; } | ||
| 923 | + .summary-tag-gold { color:var(--gold); background:rgba(212,178,84,0.1); } | ||
| 924 | + .summary-tag-entities { color:#7EB8D8; background:rgba(126,184,216,0.1); } | ||
| 925 | + .summary-tag-subarea { color:#A8C4A0; background:rgba(168,196,160,0.1); } | ||
| 926 | + .summary-period { color:var(--text-dim); font-weight:300; } | ||
| 927 | + .summary-arrow { color:var(--text-dim); font-size:12px; } | ||
| 928 | + | ||
| 929 | + /* Sort */ | ||
| 930 | + .sort-buttons { display:flex; gap:6px; flex-shrink:0; align-items:flex-start; padding-top:2px; } | ||
| 931 | + .sort-btn { display:flex; align-items:center; gap:4px; padding:5px 10px; background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.08); border-radius:6px; font-family:var(--mono); font-size:11px; color:var(--text-muted); cursor:pointer; transition:all 0.15s; } | ||
| 932 | + .sort-btn:hover { background:rgba(255,255,255,0.09); color:var(--text); } | ||
| 933 | + .sort-btn-active { background:rgba(212,178,84,0.1); border-color:rgba(212,178,84,0.3); color:var(--gold); } | ||
| 934 | + | ||
| 935 | + /* Results list */ | ||
| 936 | + .results-list { padding:4px 0; } | ||
| 937 | + .result-card { display:flex; align-items:center; gap:10px; padding:10px 16px; text-decoration:none; color:inherit; border-left:3px solid transparent; transition:all 0.2s ease; animation:fadeSlideIn 0.25s ease both; animation-delay:var(--delay,0ms); } | ||
| 938 | + @keyframes fadeSlideIn { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } } | ||
| 939 | + .result-card:hover { background:rgba(255,255,255,0.04); border-left-color:var(--gold); transform:translateX(4px); } | ||
| 940 | + .result-link-icon { flex-shrink:0; opacity:0; color:var(--gold); transition:opacity 0.2s; } | ||
| 941 | + .result-card:hover .result-link-icon { opacity:1; } | ||
| 942 | + .result-content { flex:1; min-width:0; } | ||
| 943 | + .result-text { font-size:14px; color:var(--text); line-height:1.4; margin-bottom:4px; } | ||
| 944 | + .result-text :global(mark) { background:#F5E047; color:#1A1A18; padding:1px 4px; margin:0 1px; border-radius:3px; font-weight:600; box-decoration-break:clone; -webkit-box-decoration-break:clone; } | ||
| 945 | + .result-meta { display:flex; flex-wrap:wrap; gap:10px; font-family:var(--mono); font-size:12px; } | ||
| 946 | + .result-year { color:var(--text-dim); } | ||
| 947 | + .result-monto { color:var(--gold); font-weight:500; } | ||
| 948 | + .result-da-badge { background:rgba(212,133,106,0.2); color:#D4856A; padding:2px 6px; border-radius:4px; font-size:10px; font-weight:600; letter-spacing:0.05em; } | ||
| 949 | + .result-parent { color:var(--text-muted); font-size:11px; } | ||
| 950 | + .result-subarea { color:#7EB8D8; font-size:11px; } | ||
| 951 | + .result-class-type { color:var(--text-dim); font-size:10px; text-transform:uppercase; letter-spacing:0.05em; } | ||
| 952 | + | ||
| 953 | + /* Load more */ | ||
| 954 | + .load-more-container { padding:8px 16px 16px; border-top:1px solid rgba(255,255,255,0.04); margin-top:4px; } | ||
| 955 | + .load-more-btn { display:flex; align-items:center; justify-content:center; gap:8px; width:100%; padding:12px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); border-radius:8px; font-family:var(--mono); font-size:13px; color:var(--text-muted); cursor:pointer; transition:all 0.2s; } | ||
| 956 | + .load-more-btn:hover:not(:disabled) { background:rgba(255,255,255,0.08); border-color:rgba(255,255,255,0.12); color:var(--text); } | ||
| 957 | + .load-more-btn:disabled { cursor:wait; opacity:0.7; } | ||
| 958 | + .load-more-btn .loading-dot { width:6px; height:6px; } | ||
| 959 | + | ||
| 960 | + /* Scroll hint */ | ||
| 961 | + .scroll-hint { position:absolute; bottom:28px; left:0; right:0; margin:0 auto; width:fit-content; display:flex; flex-direction:column; align-items:center; gap:10px; color:var(--text-dim); animation:scrollHint 2.5s ease-in-out infinite; z-index:1; transition:opacity 0.3s; } | ||
| 962 | + .scroll-hint-hidden { opacity:0; pointer-events:none; } | ||
| 963 | + .scroll-hint span { font-family:var(--mono); font-size:10px; letter-spacing:0.2em; text-transform:uppercase; opacity:0.7; } | ||
| 964 | + .scroll-icon-mouse { display:block; } | ||
| 965 | + .scroll-icon-touch { display:none; } | ||
| 966 | + @keyframes scrollHint { 0%,100% { transform:translateY(0); opacity:0.6; } 50% { transform:translateY(5px); opacity:1; } } | ||
| 967 | + | ||
| 968 | + /* ── Mobile ── */ | ||
| 969 | + @media (max-width: 768px) { | ||
| 970 | + .page { height:100vh; height:100dvh; } | ||
| 971 | + .section-dots { display:none; } | ||
| 972 | + .nav-logo { top:16px; left:20px; opacity:1; } | ||
| 973 | + .nav-logo img { height:60px; } | ||
| 974 | + .nav-menu-btn { top:16px; right:16px; } | ||
| 975 | + main { padding:0 20px; justify-content:center; min-height:100%; } | ||
| 976 | + .search-container { padding-top:80px; } | ||
| 977 | + .hero-title { margin-top:0; font-size:clamp(28px,8vw,40px); } | ||
| 978 | + .hero-sub { font-size:14px; margin-bottom:12px; } | ||
| 979 | + .search-bar { padding:6px 12px; } | ||
| 980 | + .search-input-wrap input { font-size:14px; padding:10px 10px 10px 6px; } | ||
| 981 | + .search-placeholder { left:6px; } | ||
| 982 | + .placeholder-text { font-size:13px; } | ||
| 983 | + .search-icon { width:18px; height:18px; } | ||
| 984 | + .filters-row { gap:6px; padding-top:8px; } | ||
| 985 | + .filter-chip { font-size:11px; padding:4px 10px; } | ||
| 986 | + .filter-chip-mode { font-size:11.5px; } | ||
| 987 | + .examples { gap:8px; } | ||
| 988 | + .example-chip { font-size:12px; padding:6px 12px; } | ||
| 989 | + .daily-hint { font-size:12px; padding:7px 12px; } | ||
| 990 | + .results-area { min-height:200px; } | ||
| 991 | + .results-content-filled { max-height:45vh; } | ||
| 992 | + .result-card { padding:8px 14px; } | ||
| 993 | + .result-card:hover { transform:none; } | ||
| 994 | + .result-text { font-size:14px; line-height:1.45; } | ||
| 995 | + .result-meta { font-size:12px; gap:10px; } | ||
| 996 | + .summary-count { font-size:16px; } | ||
| 997 | + .summary-meta { font-size:12px; } | ||
| 998 | + .scroll-icon-mouse { display:none; } | ||
| 999 | + .scroll-icon-touch { display:block; } | ||
| 1000 | + .scroll-hint { position:fixed; bottom:24px; } | ||
| 1001 | + } | ||
| 1002 | + | ||
| 1003 | + @media (max-width: 400px) { | ||
| 1004 | + .nav-logo img { height:50px; } | ||
| 1005 | + main { padding:0 16px; } | ||
| 1006 | + .search-container { padding-top:70px; } | ||
| 1007 | + .search-bar { padding:5px 10px; } | ||
| 1008 | + .search-input-wrap input { font-size:13px; padding:8px 8px 8px 4px; } | ||
| 1009 | + .placeholder-text { font-size:12px; } | ||
| 1010 | + .hero-title { font-size:26px; } | ||
| 1011 | + .hero-sub { font-size:13px; margin-bottom:10px; } | ||
| 1012 | + .results-area { min-height:180px; } | ||
| 1013 | + .results-content-filled { max-height:40vh; } | ||
| 1014 | + .scroll-hint { display:none; } | ||
| 1015 | + .examples { flex-direction:column; gap:6px; } | ||
| 1016 | + .filter-group { gap:5px; } | ||
| 1017 | + .filter-divider { display:none; } | ||
| 1018 | + } | ||
| 1019 | +</style> |
| 1 | +<script> | ||
| 2 | + import { page } from '$app/stores'; | ||
| 3 | + import { goto } from '$app/navigation'; | ||
| 4 | + import { supabase } from '$lib/supabase'; | ||
| 5 | + import { onMount } from 'svelte'; | ||
| 6 | + import * as d3 from 'd3'; | ||
| 7 | + | ||
| 8 | + // Params y query | ||
| 9 | + let codigo = $derived($page.params.codigo); | ||
| 10 | + let gestion = $derived($page.url.searchParams.get('gestion') || new Date().getFullYear().toString()); | ||
| 11 | + | ||
| 12 | + // Estado | ||
| 13 | + let loading = $state(true); | ||
| 14 | + let objetoInfo = $state(null); // Info del objeto (desc_objeto, nivel) | ||
| 15 | + let entidadesData = $state([]); | ||
| 16 | + let availableYears = $state([]); | ||
| 17 | + let selectedYear = $state(null); | ||
| 18 | + | ||
| 19 | + // Treemap | ||
| 20 | + let treemapContainer = $state(null); | ||
| 21 | + let treemapWidth = $state(0); | ||
| 22 | + let treemapHeight = $state(500); | ||
| 23 | + let treemapNodes = $state([]); | ||
| 24 | + let treemapRoot = $state(null); | ||
| 25 | + let hoveredNode = $state(null); | ||
| 26 | + let totalDevengado = $state(0); | ||
| 27 | + | ||
| 28 | + // Población para per cápita | ||
| 29 | + const POBLACION_BOLIVIA = 12006031; | ||
| 30 | + | ||
| 31 | + // Formatters | ||
| 32 | + function formatMoney(value) { | ||
| 33 | + if (value >= 1e9) return `Bs ${(value / 1e9).toFixed(1)}MM`; | ||
| 34 | + if (value >= 1e6) return `Bs ${(value / 1e6).toFixed(1)}M`; | ||
| 35 | + if (value >= 1e3) return `Bs ${(value / 1e3).toFixed(0)}K`; | ||
| 36 | + return `Bs ${value.toFixed(0)}`; | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + function formatMoneyCompact(value) { | ||
| 40 | + if (value >= 1e9) return `${(value / 1e9).toFixed(1)}MM`; | ||
| 41 | + if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`; | ||
| 42 | + if (value >= 1e3) return `${(value / 1e3).toFixed(0)}K`; | ||
| 43 | + return value.toFixed(0); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + function formatPerCapita(value) { | ||
| 47 | + const perCapita = value / POBLACION_BOLIVIA; | ||
| 48 | + return `Bs ${perCapita.toFixed(0)}/hab`; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // Colores para entidades (gradiente basado en ranking) | ||
| 52 | + function getEntityColor(index, total, opacity = 0.85) { | ||
| 53 | + // Gradiente de más oscuro (top) a más claro (bottom) | ||
| 54 | + const hue = 210; // Azul | ||
| 55 | + const saturation = 60 + (index / total) * 20; | ||
| 56 | + const lightness = 35 + (index / total) * 30; | ||
| 57 | + return `hsla(${hue}, ${saturation}%, ${lightness}%, ${opacity})`; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + function getTextColor(index, total) { | ||
| 61 | + return index < total * 0.6 ? '#ffffff' : '#1a1a1a'; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + // Cargar años disponibles | ||
| 65 | + async function loadAvailableYears() { | ||
| 66 | + const { data, error } = await supabase | ||
| 67 | + .schema('ppto') | ||
| 68 | + .from('treemap_objeto') | ||
| 69 | + .select('gestion') | ||
| 70 | + .eq('objeto', parseInt(codigo)) | ||
| 71 | + .gt('devengado', 0); | ||
| 72 | + | ||
| 73 | + if (!error && data) { | ||
| 74 | + const years = [...new Set(data.map(d => d.gestion))].sort((a, b) => b - a); | ||
| 75 | + availableYears = years; | ||
| 76 | + | ||
| 77 | + // Usar año de query param o el más reciente | ||
| 78 | + const yearParam = parseInt(gestion); | ||
| 79 | + selectedYear = years.includes(yearParam) ? yearParam : years[0]; | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + // Cargar datos de entidades para el objeto | ||
| 84 | + async function loadEntityData() { | ||
| 85 | + if (!selectedYear || !codigo) return; | ||
| 86 | + | ||
| 87 | + loading = true; | ||
| 88 | + | ||
| 89 | + try { | ||
| 90 | + // Obtener info del objeto | ||
| 91 | + const { data: infoData } = await supabase | ||
| 92 | + .schema('ppto') | ||
| 93 | + .from('treemap_objeto') | ||
| 94 | + .select('desc_objeto, nivel') | ||
| 95 | + .eq('objeto', parseInt(codigo)) | ||
| 96 | + .eq('gestion', selectedYear) | ||
| 97 | + .limit(1) | ||
| 98 | + .single(); | ||
| 99 | + | ||
| 100 | + if (infoData) { | ||
| 101 | + objetoInfo = infoData; | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | + // Obtener distribución por entidades (excluyendo entidad 0=total y 99=otros) | ||
| 105 | + const { data, error } = await supabase | ||
| 106 | + .schema('ppto') | ||
| 107 | + .from('treemap_objeto') | ||
| 108 | + .select('entidad, desc_entidad, devengado') | ||
| 109 | + .eq('objeto', parseInt(codigo)) | ||
| 110 | + .eq('gestion', selectedYear) | ||
| 111 | + .neq('entidad', 0) | ||
| 112 | + .neq('entidad', 99) | ||
| 113 | + .gt('devengado', 0) | ||
| 114 | + .order('devengado', { ascending: false }); | ||
| 115 | + | ||
| 116 | + if (error) { | ||
| 117 | + console.error('Error loading entity data:', error); | ||
| 118 | + return; | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + entidadesData = data || []; | ||
| 122 | + totalDevengado = entidadesData.reduce((sum, d) => sum + (d.devengado || 0), 0); | ||
| 123 | + | ||
| 124 | + buildTreemap(); | ||
| 125 | + } catch (err) { | ||
| 126 | + console.error('Error:', err); | ||
| 127 | + } finally { | ||
| 128 | + loading = false; | ||
| 129 | + } | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + // Construir treemap | ||
| 133 | + function buildTreemap() { | ||
| 134 | + if (!entidadesData.length || treemapWidth <= 0) return; | ||
| 135 | + | ||
| 136 | + const hierarchy = d3.hierarchy({ | ||
| 137 | + name: 'root', | ||
| 138 | + children: entidadesData.map((d, i) => ({ | ||
| 139 | + id: d.entidad, | ||
| 140 | + name: d.desc_entidad || `Entidad ${d.entidad}`, | ||
| 141 | + value: d.devengado, | ||
| 142 | + index: i | ||
| 143 | + })) | ||
| 144 | + }).sum(d => d.value || 0) | ||
| 145 | + .sort((a, b) => b.value - a.value); | ||
| 146 | + | ||
| 147 | + const treemap = d3.treemap() | ||
| 148 | + .size([treemapWidth, treemapHeight]) | ||
| 149 | + .padding(2) | ||
| 150 | + .round(true); | ||
| 151 | + | ||
| 152 | + treemap(hierarchy); | ||
| 153 | + treemapRoot = hierarchy; | ||
| 154 | + treemapNodes = hierarchy.children || []; | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + // Resize observer | ||
| 158 | + function setupResizeObserver() { | ||
| 159 | + if (!treemapContainer) return; | ||
| 160 | + | ||
| 161 | + const observer = new ResizeObserver(entries => { | ||
| 162 | + for (const entry of entries) { | ||
| 163 | + treemapWidth = entry.contentRect.width; | ||
| 164 | + buildTreemap(); | ||
| 165 | + } | ||
| 166 | + }); | ||
| 167 | + | ||
| 168 | + observer.observe(treemapContainer); | ||
| 169 | + return () => observer.disconnect(); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + // Navegación | ||
| 173 | + function goBack() { | ||
| 174 | + goto(`/clasificadores/objeto-gasto?modo=mapa`); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + function changeYear(year) { | ||
| 178 | + selectedYear = year; | ||
| 179 | + goto(`?gestion=${year}`, { replaceState: true, noScroll: true }); | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + // Efectos | ||
| 183 | + $effect(() => { | ||
| 184 | + if (codigo) { | ||
| 185 | + loadAvailableYears(); | ||
| 186 | + } | ||
| 187 | + }); | ||
| 188 | + | ||
| 189 | + $effect(() => { | ||
| 190 | + if (selectedYear && codigo) { | ||
| 191 | + loadEntityData(); | ||
| 192 | + } | ||
| 193 | + }); | ||
| 194 | + | ||
| 195 | + $effect(() => { | ||
| 196 | + if (treemapContainer) { | ||
| 197 | + return setupResizeObserver(); | ||
| 198 | + } | ||
| 199 | + }); | ||
| 200 | + | ||
| 201 | + $effect(() => { | ||
| 202 | + if (entidadesData.length && treemapWidth > 0) { | ||
| 203 | + buildTreemap(); | ||
| 204 | + } | ||
| 205 | + }); | ||
| 206 | + | ||
| 207 | + onMount(() => { | ||
| 208 | + if (treemapContainer) { | ||
| 209 | + treemapWidth = treemapContainer.clientWidth; | ||
| 210 | + } | ||
| 211 | + }); | ||
| 212 | +</script> | ||
| 213 | + | ||
| 214 | +<svelte:head> | ||
| 215 | + <title>{objetoInfo?.desc_objeto || `Objeto ${codigo}`} - Distribución por Entidades</title> | ||
| 216 | +</svelte:head> | ||
| 217 | + | ||
| 218 | +<div class="page-container"> | ||
| 219 | + <!-- Header --> | ||
| 220 | + <header class="page-header"> | ||
| 221 | + <button class="back-btn" onclick={goBack}> | ||
| 222 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 223 | + <path d="M19 12H5M12 19l-7-7 7-7"/> | ||
| 224 | + </svg> | ||
| 225 | + Volver al mapa | ||
| 226 | + </button> | ||
| 227 | + | ||
| 228 | + <div class="header-content"> | ||
| 229 | + <div class="header-badge">{objetoInfo?.nivel || 'objeto'}</div> | ||
| 230 | + <h1 class="header-title"> | ||
| 231 | + <span class="header-code">{codigo}</span> | ||
| 232 | + <span class="header-separator">·</span> | ||
| 233 | + <span class="header-name">{objetoInfo?.desc_objeto || 'Cargando...'}</span> | ||
| 234 | + </h1> | ||
| 235 | + <p class="header-subtitle">¿Qué entidades gastan más en esto?</p> | ||
| 236 | + </div> | ||
| 237 | + | ||
| 238 | + <!-- Selector de año --> | ||
| 239 | + <div class="year-selector"> | ||
| 240 | + <label>Gestión:</label> | ||
| 241 | + <select bind:value={selectedYear} onchange={(e) => changeYear(parseInt(e.target.value))}> | ||
| 242 | + {#each availableYears as year} | ||
| 243 | + <option value={year}>{year}</option> | ||
| 244 | + {/each} | ||
| 245 | + </select> | ||
| 246 | + </div> | ||
| 247 | + </header> | ||
| 248 | + | ||
| 249 | + <!-- Stats resumen --> | ||
| 250 | + {#if !loading && entidadesData.length > 0} | ||
| 251 | + <div class="stats-bar"> | ||
| 252 | + <div class="stat"> | ||
| 253 | + <span class="stat-value">{entidadesData.length}</span> | ||
| 254 | + <span class="stat-label">entidades</span> | ||
| 255 | + </div> | ||
| 256 | + <div class="stat"> | ||
| 257 | + <span class="stat-value">{formatMoney(totalDevengado)}</span> | ||
| 258 | + <span class="stat-label">total ejecutado</span> | ||
| 259 | + </div> | ||
| 260 | + <div class="stat"> | ||
| 261 | + <span class="stat-value">{formatPerCapita(totalDevengado)}</span> | ||
| 262 | + <span class="stat-label">per cápita</span> | ||
| 263 | + </div> | ||
| 264 | + </div> | ||
| 265 | + {/if} | ||
| 266 | + | ||
| 267 | + <!-- Treemap --> | ||
| 268 | + <div class="treemap-section"> | ||
| 269 | + <div | ||
| 270 | + bind:this={treemapContainer} | ||
| 271 | + class="treemap-container" | ||
| 272 | + style="height: {treemapHeight}px;" | ||
| 273 | + > | ||
| 274 | + {#if loading} | ||
| 275 | + <div class="loading-state"> | ||
| 276 | + <div class="loader"></div> | ||
| 277 | + <p>Cargando distribución...</p> | ||
| 278 | + </div> | ||
| 279 | + {:else if treemapNodes.length > 0} | ||
| 280 | + <svg width={treemapWidth} height={treemapHeight} class="block"> | ||
| 281 | + {#each treemapNodes as node, i} | ||
| 282 | + {@const width = node.x1 - node.x0} | ||
| 283 | + {@const height = node.y1 - node.y0} | ||
| 284 | + {@const isHovered = hoveredNode === node} | ||
| 285 | + {@const pct = (node.value / totalDevengado) * 100} | ||
| 286 | + {@const area = width * height} | ||
| 287 | + {@const scale = Math.sqrt(area) / 12} | ||
| 288 | + {@const pctSize = Math.min(22, Math.max(10, scale * 1.6))} | ||
| 289 | + {@const nameSize = Math.min(14, Math.max(8, scale * 1.0))} | ||
| 290 | + {@const valueSize = Math.min(12, Math.max(7, scale * 0.8))} | ||
| 291 | + {@const textColor = getTextColor(i, treemapNodes.length)} | ||
| 292 | + | ||
| 293 | + <g | ||
| 294 | + class="treemap-node" | ||
| 295 | + transform="translate({node.x0}, {node.y0})" | ||
| 296 | + onmouseenter={() => hoveredNode = node} | ||
| 297 | + onmouseleave={() => hoveredNode = null} | ||
| 298 | + > | ||
| 299 | + <rect | ||
| 300 | + width={width} | ||
| 301 | + height={height} | ||
| 302 | + fill={getEntityColor(i, treemapNodes.length, isHovered ? 1 : 0.85)} | ||
| 303 | + stroke={isHovered ? 'var(--theme-titulo)' : 'transparent'} | ||
| 304 | + stroke-width={isHovered ? 2 : 0} | ||
| 305 | + rx="3" | ||
| 306 | + /> | ||
| 307 | + | ||
| 308 | + {#if width > 40 && height > 25} | ||
| 309 | + <foreignObject x="4" y="4" width={width - 8} height={height - 8}> | ||
| 310 | + <div class="node-content" style="color: {textColor};"> | ||
| 311 | + <div class="node-pct" style="font-size: {pctSize}px;"> | ||
| 312 | + {pct >= 10 ? pct.toFixed(0) : pct >= 1 ? pct.toFixed(1) : pct.toFixed(2)}% | ||
| 313 | + </div> | ||
| 314 | + {#if height > 45 && width > 60} | ||
| 315 | + <div class="node-name" style="font-size: {nameSize}px;"> | ||
| 316 | + {width > 120 ? node.data.name : node.data.name.slice(0, Math.floor(width / 7)) + (node.data.name.length > Math.floor(width / 7) ? '…' : '')} | ||
| 317 | + </div> | ||
| 318 | + {/if} | ||
| 319 | + {#if height > 65 && width > 70} | ||
| 320 | + <div class="node-value" style="font-size: {valueSize}px;"> | ||
| 321 | + {formatMoneyCompact(node.value)} | ||
| 322 | + </div> | ||
| 323 | + {/if} | ||
| 324 | + </div> | ||
| 325 | + </foreignObject> | ||
| 326 | + {/if} | ||
| 327 | + </g> | ||
| 328 | + {/each} | ||
| 329 | + </svg> | ||
| 330 | + | ||
| 331 | + <!-- Tooltip --> | ||
| 332 | + {#if hoveredNode} | ||
| 333 | + {@const pct = (hoveredNode.value / totalDevengado) * 100} | ||
| 334 | + <div | ||
| 335 | + class="treemap-tooltip" | ||
| 336 | + style=" | ||
| 337 | + left: {Math.min(hoveredNode.x0 + 10, treemapWidth - 280)}px; | ||
| 338 | + top: {Math.min(hoveredNode.y0 + 10, treemapHeight - 100)}px; | ||
| 339 | + " | ||
| 340 | + > | ||
| 341 | + <div class="tooltip-rank">#{treemapNodes.indexOf(hoveredNode) + 1} de {treemapNodes.length}</div> | ||
| 342 | + <div class="tooltip-name">{hoveredNode.data.name}</div> | ||
| 343 | + <div class="tooltip-value">{formatMoney(hoveredNode.value)}</div> | ||
| 344 | + <div class="tooltip-pct">{pct.toFixed(1)}% del total · {formatPerCapita(hoveredNode.value)}</div> | ||
| 345 | + </div> | ||
| 346 | + {/if} | ||
| 347 | + {:else} | ||
| 348 | + <div class="empty-state"> | ||
| 349 | + <p>No hay datos de entidades para este objeto en {selectedYear}</p> | ||
| 350 | + </div> | ||
| 351 | + {/if} | ||
| 352 | + </div> | ||
| 353 | + </div> | ||
| 354 | + | ||
| 355 | + <!-- Ranking list --> | ||
| 356 | + {#if !loading && entidadesData.length > 0} | ||
| 357 | + <div class="ranking-section"> | ||
| 358 | + <h2 class="ranking-title">Ranking de entidades</h2> | ||
| 359 | + <div class="ranking-list"> | ||
| 360 | + {#each entidadesData.slice(0, 20) as entity, i} | ||
| 361 | + {@const pct = (entity.devengado / totalDevengado) * 100} | ||
| 362 | + <div class="ranking-item"> | ||
| 363 | + <span class="ranking-pos">#{i + 1}</span> | ||
| 364 | + <div class="ranking-bar-container"> | ||
| 365 | + <div class="ranking-bar" style="width: {pct}%; background: {getEntityColor(i, entidadesData.length)};"></div> | ||
| 366 | + <div class="ranking-info"> | ||
| 367 | + <span class="ranking-name">{entity.desc_entidad || `Entidad ${entity.entidad}`}</span> | ||
| 368 | + <span class="ranking-value">{formatMoney(entity.devengado)} <span class="ranking-pct">({pct.toFixed(1)}%)</span></span> | ||
| 369 | + </div> | ||
| 370 | + </div> | ||
| 371 | + </div> | ||
| 372 | + {/each} | ||
| 373 | + {#if entidadesData.length > 20} | ||
| 374 | + <p class="ranking-more">+ {entidadesData.length - 20} entidades más</p> | ||
| 375 | + {/if} | ||
| 376 | + </div> | ||
| 377 | + </div> | ||
| 378 | + {/if} | ||
| 379 | +</div> | ||
| 380 | + | ||
| 381 | +<style> | ||
| 382 | + .page-container { | ||
| 383 | + min-height: 100vh; | ||
| 384 | + background: var(--theme-fondo); | ||
| 385 | + padding: 1.5rem; | ||
| 386 | + max-width: 1400px; | ||
| 387 | + margin: 0 auto; | ||
| 388 | + } | ||
| 389 | + | ||
| 390 | + /* Header */ | ||
| 391 | + .page-header { | ||
| 392 | + margin-bottom: 1.5rem; | ||
| 393 | + } | ||
| 394 | + | ||
| 395 | + .back-btn { | ||
| 396 | + display: inline-flex; | ||
| 397 | + align-items: center; | ||
| 398 | + gap: 0.5rem; | ||
| 399 | + padding: 0.5rem 1rem; | ||
| 400 | + background: var(--theme-surface); | ||
| 401 | + border: 1px solid var(--theme-borde); | ||
| 402 | + border-radius: 8px; | ||
| 403 | + color: var(--theme-texto); | ||
| 404 | + font-size: 0.875rem; | ||
| 405 | + cursor: pointer; | ||
| 406 | + transition: all 0.15s ease; | ||
| 407 | + margin-bottom: 1rem; | ||
| 408 | + } | ||
| 409 | + .back-btn:hover { | ||
| 410 | + background: var(--theme-fill); | ||
| 411 | + border-color: var(--theme-accent); | ||
| 412 | + color: var(--theme-accent); | ||
| 413 | + } | ||
| 414 | + | ||
| 415 | + .header-content { | ||
| 416 | + margin-bottom: 1rem; | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + .header-badge { | ||
| 420 | + display: inline-block; | ||
| 421 | + padding: 0.25rem 0.75rem; | ||
| 422 | + background: var(--theme-accent); | ||
| 423 | + color: white; | ||
| 424 | + font-size: 0.75rem; | ||
| 425 | + font-weight: 600; | ||
| 426 | + text-transform: uppercase; | ||
| 427 | + letter-spacing: 0.5px; | ||
| 428 | + border-radius: 4px; | ||
| 429 | + margin-bottom: 0.5rem; | ||
| 430 | + } | ||
| 431 | + | ||
| 432 | + .header-title { | ||
| 433 | + font-size: 1.5rem; | ||
| 434 | + font-weight: 600; | ||
| 435 | + color: var(--theme-titulo); | ||
| 436 | + margin: 0 0 0.25rem 0; | ||
| 437 | + line-height: 1.3; | ||
| 438 | + } | ||
| 439 | + | ||
| 440 | + .header-code { | ||
| 441 | + font-family: var(--font-mono); | ||
| 442 | + opacity: 0.7; | ||
| 443 | + } | ||
| 444 | + .header-separator { | ||
| 445 | + opacity: 0.4; | ||
| 446 | + margin: 0 0.25rem; | ||
| 447 | + } | ||
| 448 | + | ||
| 449 | + .header-subtitle { | ||
| 450 | + font-size: 1rem; | ||
| 451 | + color: var(--theme-texto); | ||
| 452 | + opacity: 0.7; | ||
| 453 | + margin: 0; | ||
| 454 | + } | ||
| 455 | + | ||
| 456 | + .year-selector { | ||
| 457 | + display: flex; | ||
| 458 | + align-items: center; | ||
| 459 | + gap: 0.5rem; | ||
| 460 | + } | ||
| 461 | + .year-selector label { | ||
| 462 | + font-size: 0.875rem; | ||
| 463 | + color: var(--theme-texto); | ||
| 464 | + } | ||
| 465 | + .year-selector select { | ||
| 466 | + padding: 0.5rem 1rem; | ||
| 467 | + background: var(--theme-surface); | ||
| 468 | + border: 1px solid var(--theme-borde); | ||
| 469 | + border-radius: 6px; | ||
| 470 | + color: var(--theme-titulo); | ||
| 471 | + font-size: 0.875rem; | ||
| 472 | + cursor: pointer; | ||
| 473 | + } | ||
| 474 | + | ||
| 475 | + /* Stats bar */ | ||
| 476 | + .stats-bar { | ||
| 477 | + display: flex; | ||
| 478 | + gap: 2rem; | ||
| 479 | + padding: 1rem 1.5rem; | ||
| 480 | + background: var(--theme-surface); | ||
| 481 | + border: 1px solid var(--theme-borde); | ||
| 482 | + border-radius: 12px; | ||
| 483 | + margin-bottom: 1.5rem; | ||
| 484 | + } | ||
| 485 | + | ||
| 486 | + .stat { | ||
| 487 | + display: flex; | ||
| 488 | + flex-direction: column; | ||
| 489 | + } | ||
| 490 | + .stat-value { | ||
| 491 | + font-size: 1.25rem; | ||
| 492 | + font-weight: 600; | ||
| 493 | + color: var(--theme-titulo); | ||
| 494 | + font-variant-numeric: tabular-nums; | ||
| 495 | + } | ||
| 496 | + .stat-label { | ||
| 497 | + font-size: 0.75rem; | ||
| 498 | + color: var(--theme-texto); | ||
| 499 | + opacity: 0.7; | ||
| 500 | + text-transform: uppercase; | ||
| 501 | + letter-spacing: 0.5px; | ||
| 502 | + } | ||
| 503 | + | ||
| 504 | + /* Treemap */ | ||
| 505 | + .treemap-section { | ||
| 506 | + margin-bottom: 2rem; | ||
| 507 | + } | ||
| 508 | + | ||
| 509 | + .treemap-container { | ||
| 510 | + position: relative; | ||
| 511 | + background: var(--theme-surface); | ||
| 512 | + border: 1px solid var(--theme-borde); | ||
| 513 | + border-radius: 12px; | ||
| 514 | + overflow: hidden; | ||
| 515 | + } | ||
| 516 | + | ||
| 517 | + .loading-state, | ||
| 518 | + .empty-state { | ||
| 519 | + position: absolute; | ||
| 520 | + inset: 0; | ||
| 521 | + display: flex; | ||
| 522 | + flex-direction: column; | ||
| 523 | + align-items: center; | ||
| 524 | + justify-content: center; | ||
| 525 | + color: var(--theme-texto); | ||
| 526 | + opacity: 0.7; | ||
| 527 | + } | ||
| 528 | + | ||
| 529 | + .loader { | ||
| 530 | + width: 40px; | ||
| 531 | + height: 40px; | ||
| 532 | + border: 3px solid var(--theme-borde); | ||
| 533 | + border-top-color: var(--theme-accent); | ||
| 534 | + border-radius: 50%; | ||
| 535 | + animation: spin 1s linear infinite; | ||
| 536 | + margin-bottom: 1rem; | ||
| 537 | + } | ||
| 538 | + | ||
| 539 | + @keyframes spin { | ||
| 540 | + to { transform: rotate(360deg); } | ||
| 541 | + } | ||
| 542 | + | ||
| 543 | + :global(.treemap-node) { | ||
| 544 | + transition: opacity 0.15s ease; | ||
| 545 | + } | ||
| 546 | + :global(.treemap-node rect) { | ||
| 547 | + transition: fill 0.15s ease; | ||
| 548 | + } | ||
| 549 | + | ||
| 550 | + .node-content { | ||
| 551 | + display: flex; | ||
| 552 | + flex-direction: column; | ||
| 553 | + gap: 0; | ||
| 554 | + overflow: hidden; | ||
| 555 | + font-family: var(--font-sans); | ||
| 556 | + } | ||
| 557 | + | ||
| 558 | + .node-pct { | ||
| 559 | + font-weight: 600; | ||
| 560 | + line-height: 1; | ||
| 561 | + opacity: 0.9; | ||
| 562 | + } | ||
| 563 | + | ||
| 564 | + .node-name { | ||
| 565 | + line-height: 1.2; | ||
| 566 | + margin-top: 2px; | ||
| 567 | + opacity: 0.85; | ||
| 568 | + } | ||
| 569 | + | ||
| 570 | + .node-value { | ||
| 571 | + font-variant-numeric: tabular-nums; | ||
| 572 | + opacity: 0.7; | ||
| 573 | + margin-top: 2px; | ||
| 574 | + } | ||
| 575 | + | ||
| 576 | + /* Tooltip */ | ||
| 577 | + .treemap-tooltip { | ||
| 578 | + position: absolute; | ||
| 579 | + pointer-events: none; | ||
| 580 | + padding: 0.75rem 1rem; | ||
| 581 | + border-radius: 10px; | ||
| 582 | + z-index: 10; | ||
| 583 | + max-width: 280px; | ||
| 584 | + background: rgba(255, 255, 255, 0.85); | ||
| 585 | + backdrop-filter: blur(12px); | ||
| 586 | + -webkit-backdrop-filter: blur(12px); | ||
| 587 | + border: 1px solid rgba(255, 255, 255, 0.3); | ||
| 588 | + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | ||
| 589 | + color: #1a1a1a; | ||
| 590 | + font-family: var(--font-sans); | ||
| 591 | + } | ||
| 592 | + | ||
| 593 | + :global(html.dark) .treemap-tooltip { | ||
| 594 | + background: rgba(30, 30, 30, 0.85); | ||
| 595 | + border: 1px solid rgba(255, 255, 255, 0.1); | ||
| 596 | + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | ||
| 597 | + color: #f5f5f5; | ||
| 598 | + } | ||
| 599 | + | ||
| 600 | + .tooltip-rank { | ||
| 601 | + font-size: 0.7rem; | ||
| 602 | + font-weight: 600; | ||
| 603 | + text-transform: uppercase; | ||
| 604 | + letter-spacing: 0.5px; | ||
| 605 | + opacity: 0.6; | ||
| 606 | + margin-bottom: 0.25rem; | ||
| 607 | + } | ||
| 608 | + | ||
| 609 | + .tooltip-name { | ||
| 610 | + font-size: 0.9rem; | ||
| 611 | + font-weight: 500; | ||
| 612 | + line-height: 1.3; | ||
| 613 | + } | ||
| 614 | + | ||
| 615 | + .tooltip-value { | ||
| 616 | + font-size: 0.875rem; | ||
| 617 | + font-weight: 600; | ||
| 618 | + margin-top: 0.5rem; | ||
| 619 | + font-variant-numeric: tabular-nums; | ||
| 620 | + } | ||
| 621 | + | ||
| 622 | + .tooltip-pct { | ||
| 623 | + font-size: 0.75rem; | ||
| 624 | + opacity: 0.7; | ||
| 625 | + font-variant-numeric: tabular-nums; | ||
| 626 | + } | ||
| 627 | + | ||
| 628 | + /* Ranking list */ | ||
| 629 | + .ranking-section { | ||
| 630 | + background: var(--theme-surface); | ||
| 631 | + border: 1px solid var(--theme-borde); | ||
| 632 | + border-radius: 12px; | ||
| 633 | + padding: 1.5rem; | ||
| 634 | + } | ||
| 635 | + | ||
| 636 | + .ranking-title { | ||
| 637 | + font-size: 1rem; | ||
| 638 | + font-weight: 600; | ||
| 639 | + color: var(--theme-titulo); | ||
| 640 | + margin: 0 0 1rem 0; | ||
| 641 | + } | ||
| 642 | + | ||
| 643 | + .ranking-list { | ||
| 644 | + display: flex; | ||
| 645 | + flex-direction: column; | ||
| 646 | + gap: 0.75rem; | ||
| 647 | + } | ||
| 648 | + | ||
| 649 | + .ranking-item { | ||
| 650 | + display: grid; | ||
| 651 | + grid-template-columns: 2.5rem 1fr; | ||
| 652 | + gap: 0.75rem; | ||
| 653 | + align-items: center; | ||
| 654 | + } | ||
| 655 | + | ||
| 656 | + .ranking-pos { | ||
| 657 | + font-size: 0.875rem; | ||
| 658 | + font-weight: 600; | ||
| 659 | + color: var(--theme-texto); | ||
| 660 | + opacity: 0.5; | ||
| 661 | + font-variant-numeric: tabular-nums; | ||
| 662 | + } | ||
| 663 | + | ||
| 664 | + .ranking-bar-container { | ||
| 665 | + position: relative; | ||
| 666 | + height: 32px; | ||
| 667 | + background: var(--theme-fill); | ||
| 668 | + border-radius: 4px; | ||
| 669 | + overflow: hidden; | ||
| 670 | + } | ||
| 671 | + | ||
| 672 | + .ranking-bar { | ||
| 673 | + position: absolute; | ||
| 674 | + left: 0; | ||
| 675 | + top: 0; | ||
| 676 | + height: 100%; | ||
| 677 | + border-radius: 4px; | ||
| 678 | + transition: width 0.3s ease; | ||
| 679 | + } | ||
| 680 | + | ||
| 681 | + .ranking-info { | ||
| 682 | + position: absolute; | ||
| 683 | + left: 0.75rem; | ||
| 684 | + right: 0.75rem; | ||
| 685 | + top: 50%; | ||
| 686 | + transform: translateY(-50%); | ||
| 687 | + display: flex; | ||
| 688 | + justify-content: space-between; | ||
| 689 | + align-items: center; | ||
| 690 | + pointer-events: none; | ||
| 691 | + } | ||
| 692 | + | ||
| 693 | + .ranking-name { | ||
| 694 | + font-size: 0.8125rem; | ||
| 695 | + font-weight: 500; | ||
| 696 | + color: var(--theme-titulo); | ||
| 697 | + white-space: nowrap; | ||
| 698 | + overflow: hidden; | ||
| 699 | + text-overflow: ellipsis; | ||
| 700 | + max-width: 60%; | ||
| 701 | + } | ||
| 702 | + | ||
| 703 | + .ranking-value { | ||
| 704 | + font-size: 0.8125rem; | ||
| 705 | + font-weight: 500; | ||
| 706 | + color: var(--theme-titulo); | ||
| 707 | + font-variant-numeric: tabular-nums; | ||
| 708 | + } | ||
| 709 | + | ||
| 710 | + .ranking-pct { | ||
| 711 | + opacity: 0.6; | ||
| 712 | + font-weight: 400; | ||
| 713 | + } | ||
| 714 | + | ||
| 715 | + .ranking-more { | ||
| 716 | + font-size: 0.8125rem; | ||
| 717 | + color: var(--theme-texto); | ||
| 718 | + opacity: 0.6; | ||
| 719 | + text-align: center; | ||
| 720 | + margin: 0.5rem 0 0 0; | ||
| 721 | + } | ||
| 722 | + | ||
| 723 | + /* Responsive */ | ||
| 724 | + @media (max-width: 640px) { | ||
| 725 | + .page-container { | ||
| 726 | + padding: 1rem; | ||
| 727 | + } | ||
| 728 | + | ||
| 729 | + .header-title { | ||
| 730 | + font-size: 1.25rem; | ||
| 731 | + } | ||
| 732 | + | ||
| 733 | + .stats-bar { | ||
| 734 | + flex-wrap: wrap; | ||
| 735 | + gap: 1rem; | ||
| 736 | + } | ||
| 737 | + | ||
| 738 | + .stat-value { | ||
| 739 | + font-size: 1.1rem; | ||
| 740 | + } | ||
| 741 | + | ||
| 742 | + .ranking-item { | ||
| 743 | + grid-template-columns: 2rem 1fr; | ||
| 744 | + } | ||
| 745 | + | ||
| 746 | + .ranking-name { | ||
| 747 | + max-width: 50%; | ||
| 748 | + } | ||
| 749 | + } | ||
| 750 | +</style> |
| 1 | <script> | 1 | <script> |
| 2 | import { onMount } from 'svelte'; | 2 | import { onMount } from 'svelte'; |
| 3 | import * as d3 from 'd3'; | 3 | import * as d3 from 'd3'; |
| 4 | + import * as Plot from '@observablehq/plot'; | ||
| 5 | + | ||
| 6 | + // Entidad de ejemplo | ||
| 7 | + const entidad = { | ||
| 8 | + codigo: 139, | ||
| 9 | + nombre: 'Universidad Mayor de San Andrés', | ||
| 10 | + sigla: 'UMSA', | ||
| 11 | + sector: 'Universidades Públicas' | ||
| 12 | + }; | ||
| 4 | 13 | ||
| 5 | // Estado | 14 | // Estado |
| 6 | - let streamData = $state([]); | 15 | + let gestionSeleccionada = $state(2024); |
| 7 | - let loading = $state(true); | 16 | + const gestiones = [2020, 2021, 2022, 2023, 2024, 2025]; |
| 8 | - let container = $state(null); | ||
| 9 | - let width = $state(0); | ||
| 10 | - let height = $state(500); | ||
| 11 | - let tooltip = $state({ show: false, x: 0, y: 0, data: null }); | ||
| 12 | - | ||
| 13 | - // Drilldown | ||
| 14 | - let currentLevel = $state('tipo'); // tipo -> clase -> cuenta -> subcuenta | ||
| 15 | - let breadcrumb = $state([{ level: 'tipo', codigo: null, descripcion: 'Todos los ingresos' }]); | ||
| 16 | - let currentParent = $state(null); | ||
| 17 | - | ||
| 18 | - // Colores para las categorías | ||
| 19 | - const colorScale = d3.scaleOrdinal() | ||
| 20 | - .range([ | ||
| 21 | - '#4A8B6E', '#C9A751', '#8B6E9B', '#6B9FD4', '#D4846B', | ||
| 22 | - '#5B8FA8', '#A8845B', '#7B9B6B', '#9B7B8B', '#6B8B9B', | ||
| 23 | - '#B89B6B', '#6B7B9B', '#9B8B6B', '#7B8B7B', '#8B7B6B', | ||
| 24 | - '#6B9B8B', '#9B6B7B', '#7B6B9B', '#8B9B6B', '#6B8B7B' | ||
| 25 | - ]); | ||
| 26 | - | ||
| 27 | - // Cargar datos | ||
| 28 | - async function loadData() { | ||
| 29 | - loading = true; | ||
| 30 | - try { | ||
| 31 | - const response = await fetch('/stream1.csv'); | ||
| 32 | - const text = await response.text(); | ||
| 33 | - const parsed = d3.csvParse(text, d => ({ | ||
| 34 | - entidad: +d.entidad, | ||
| 35 | - año: +d.año, | ||
| 36 | - nivel: d.nivel, | ||
| 37 | - codigo: +d.codigo, | ||
| 38 | - descripcion: d.descripcion, | ||
| 39 | - codigo_padre: d.codigo_padre ? +d.codigo_padre : null, | ||
| 40 | - desc_padre: d.desc_padre || null, | ||
| 41 | - devengado: +d.devengado | ||
| 42 | - })); | ||
| 43 | 17 | ||
| 44 | - // Filtrar solo entidad 0 (Todo el Estado) | 18 | + // Datos mock - Historia temporal (20 años) |
| 45 | - streamData = parsed.filter(d => d.entidad === 0); | 19 | + const historiaIngresos = Array.from({ length: 20 }, (_, i) => ({ |
| 46 | - console.log('Stream data loaded:', streamData.length, 'rows'); | 20 | + gestion: 2005 + i, |
| 47 | - } catch (err) { | 21 | + monto: 800000000 + Math.random() * 400000000 + i * 50000000 |
| 48 | - console.error('Error loading data:', err); | 22 | + })); |
| 49 | - } | ||
| 50 | - loading = false; | ||
| 51 | - } | ||
| 52 | 23 | ||
| 53 | - // Obtener datos filtrados para el nivel actual | 24 | + const historiaGastos = Array.from({ length: 20 }, (_, i) => ({ |
| 54 | - function getFilteredData() { | 25 | + gestion: 2005 + i, |
| 55 | - let filtered = streamData.filter(d => d.nivel === currentLevel); | 26 | + monto: 750000000 + Math.random() * 350000000 + i * 45000000 |
| 27 | + })); | ||
| 56 | 28 | ||
| 57 | - if (currentParent !== null) { | 29 | + // Datos mock - Composición del gasto |
| 58 | - filtered = filtered.filter(d => d.codigo_padre === currentParent); | 30 | + const gastoObjetos = [ |
| 59 | - } | 31 | + { codigo: '10000', nombre: 'Servicios Personales', monto: 850000000, padre: 'Gasto Corriente' }, |
| 32 | + { codigo: '20000', nombre: 'Servicios No Personales', monto: 180000000, padre: 'Gasto Corriente' }, | ||
| 33 | + { codigo: '30000', nombre: 'Materiales y Suministros', monto: 120000000, padre: 'Gasto Corriente' }, | ||
| 34 | + { codigo: '40000', nombre: 'Activos Reales', monto: 95000000, padre: 'Inversión' }, | ||
| 35 | + { codigo: '70000', nombre: 'Transferencias', monto: 45000000, padre: 'Transferencias' }, | ||
| 36 | + { codigo: '80000', nombre: 'Impuestos y Otros', monto: 25000000, padre: 'Otros' } | ||
| 37 | + ]; | ||
| 38 | + | ||
| 39 | + const gastoFinalidad = [ | ||
| 40 | + { codigo: '22', nombre: 'Educación Superior', monto: 920000000, padre: 'Educación' }, | ||
| 41 | + { codigo: '23', nombre: 'Investigación', monto: 180000000, padre: 'Educación' }, | ||
| 42 | + { codigo: '14', nombre: 'Administración', monto: 150000000, padre: 'Servicios Generales' }, | ||
| 43 | + { codigo: '31', nombre: 'Salud', monto: 45000000, padre: 'Servicios Sociales' }, | ||
| 44 | + { codigo: '42', nombre: 'Extensión', monto: 20000000, padre: 'Cultura' } | ||
| 45 | + ]; | ||
| 46 | + | ||
| 47 | + const gastoSectores = [ | ||
| 48 | + { codigo: 'EDU', nombre: 'Educación', monto: 1050000000, padre: 'Social' }, | ||
| 49 | + { codigo: 'ADM', nombre: 'Administración Pública', monto: 180000000, padre: 'Gubernamental' }, | ||
| 50 | + { codigo: 'SAL', nombre: 'Salud', monto: 55000000, padre: 'Social' }, | ||
| 51 | + { codigo: 'CUL', nombre: 'Cultura y Deporte', monto: 30000000, padre: 'Social' } | ||
| 52 | + ]; | ||
| 53 | + | ||
| 54 | + // Datos mock - Origen del dinero | ||
| 55 | + const ingresoRubros = [ | ||
| 56 | + { codigo: '1200', nombre: 'Transferencias TGN', monto: 650000000, padre: 'Transferencias' }, | ||
| 57 | + { codigo: '1400', nombre: 'Venta de Servicios', monto: 280000000, padre: 'Ingresos Propios' }, | ||
| 58 | + { codigo: '1100', nombre: 'Matrículas y Aranceles', monto: 150000000, padre: 'Ingresos Propios' }, | ||
| 59 | + { codigo: '1300', nombre: 'Regalías e IDH', monto: 95000000, padre: 'Coparticipación' }, | ||
| 60 | + { codigo: '1900', nombre: 'Otros Ingresos', monto: 40000000, padre: 'Otros' } | ||
| 61 | + ]; | ||
| 62 | + | ||
| 63 | + const ingresoOrganismos = [ | ||
| 64 | + { codigo: 'TGN', nombre: 'Tesoro General de la Nación', monto: 720000000, padre: 'Gobierno Central' }, | ||
| 65 | + { codigo: 'PROP', nombre: 'Recursos Propios', monto: 350000000, padre: 'Autogestión' }, | ||
| 66 | + { codigo: 'IDH', nombre: 'Impuesto Directo a Hidrocarburos', monto: 95000000, padre: 'Coparticipación' }, | ||
| 67 | + { codigo: 'COOP', nombre: 'Cooperación Internacional', monto: 50000000, padre: 'Externo' } | ||
| 68 | + ]; | ||
| 69 | + | ||
| 70 | + // Ranking mock | ||
| 71 | + const ranking = { | ||
| 72 | + gastos: { posicion: 23, total: 647 }, | ||
| 73 | + ingresos: { posicion: 28, total: 647 } | ||
| 74 | + }; | ||
| 60 | 75 | ||
| 61 | - return filtered; | 76 | + // Contenedores para barras D3 |
| 77 | + let barContainers = $state({}); | ||
| 78 | + let hoverData = $state({}); | ||
| 79 | + | ||
| 80 | + // Contenedores para gráficos de historia | ||
| 81 | + let chartIngresosContainer = $state(null); | ||
| 82 | + let chartGastosContainer = $state(null); | ||
| 83 | + | ||
| 84 | + // Clasificadores de gasto | ||
| 85 | + const clasificadoresGasto = [ | ||
| 86 | + { key: 'objetos', label: 'Objetos de gasto', data: gastoObjetos }, | ||
| 87 | + { key: 'finalidad', label: 'Finalidad y función', data: gastoFinalidad }, | ||
| 88 | + { key: 'sectores', label: 'Sectores económicos', data: gastoSectores } | ||
| 89 | + ]; | ||
| 90 | + | ||
| 91 | + // Clasificadores de ingreso | ||
| 92 | + const clasificadoresIngreso = [ | ||
| 93 | + { key: 'rubros', label: 'Rubros de ingreso', data: ingresoRubros }, | ||
| 94 | + { key: 'organismos', label: 'Organismos financiadores', data: ingresoOrganismos } | ||
| 95 | + ]; | ||
| 96 | + | ||
| 97 | + // Totales | ||
| 98 | + function getTotal(data) { | ||
| 99 | + return data.reduce((sum, d) => sum + d.monto, 0); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + // Formatear montos | ||
| 103 | + function formatearMonto(valor) { | ||
| 104 | + if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)} mil millones`; | ||
| 105 | + if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)} millones`; | ||
| 106 | + if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)} mil`; | ||
| 107 | + return valor.toFixed(0); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + function formatearMontoCorto(valor) { | ||
| 111 | + if (valor >= 1e9) return `${(valor / 1e9).toFixed(1)}B`; | ||
| 112 | + if (valor >= 1e6) return `${(valor / 1e6).toFixed(0)}M`; | ||
| 113 | + if (valor >= 1e3) return `${(valor / 1e3).toFixed(0)}K`; | ||
| 114 | + return valor.toFixed(0); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + // Colores del tema | ||
| 118 | + function getChartColors() { | ||
| 119 | + const root = document.documentElement; | ||
| 120 | + const isDark = root.classList.contains('dark'); | ||
| 121 | + return { | ||
| 122 | + barDefault: isDark ? '#3a3a38' : '#e0e0e0', | ||
| 123 | + barHighlight: isDark ? '#C9A751' : '#F3EAD8', | ||
| 124 | + barStroke: isDark ? '#5a5a55' : '#b0b0b0', | ||
| 125 | + barStrokeDefault: isDark ? '#2a2a28' : 'white' | ||
| 126 | + }; | ||
| 62 | } | 127 | } |
| 63 | 128 | ||
| 64 | - // Preparar datos para el streamgraph | 129 | + // Renderizar barras D3 |
| 65 | - function prepareStreamData() { | 130 | + function renderizarBarras(key, data) { |
| 66 | - const filtered = getFilteredData(); | 131 | + const container = barContainers[key]; |
| 67 | - if (!filtered.length) return { years: [], series: [], keys: [] }; | 132 | + if (!container) return; |
| 68 | - | ||
| 69 | - // Agrupar por año | ||
| 70 | - const byYear = d3.group(filtered, d => d.año); | ||
| 71 | - const years = Array.from(byYear.keys()).sort((a, b) => a - b); | ||
| 72 | - | ||
| 73 | - // Obtener todas las categorías únicas | ||
| 74 | - const categories = [...new Set(filtered.map(d => d.codigo))]; | ||
| 75 | 133 | ||
| 76 | - // Crear estructura para stack | 134 | + const total = getTotal(data); |
| 77 | - const stackData = years.map(year => { | 135 | + const height = 48; |
| 78 | - const yearData = byYear.get(year) || []; | 136 | + const radius = 4; |
| 79 | - const row = { año: year }; | 137 | + |
| 80 | - categories.forEach(cat => { | 138 | + d3.select(container).selectAll('*').remove(); |
| 81 | - const item = yearData.find(d => d.codigo === cat); | 139 | + |
| 82 | - row[cat] = item ? item.devengado : 0; | 140 | + const containerRect = container.getBoundingClientRect(); |
| 141 | + const width = containerRect.width || 800; | ||
| 142 | + | ||
| 143 | + const svg = d3.select(container) | ||
| 144 | + .append('svg') | ||
| 145 | + .attr('width', '100%') | ||
| 146 | + .attr('height', height) | ||
| 147 | + .attr('viewBox', `0 0 ${width} ${height}`) | ||
| 148 | + .attr('preserveAspectRatio', 'none') | ||
| 149 | + .style('display', 'block'); | ||
| 150 | + | ||
| 151 | + let cumulative = 0; | ||
| 152 | + const segments = data.map(d => { | ||
| 153 | + const segWidth = (d.monto / total) * width; | ||
| 154 | + const segment = { ...d, x: cumulative, width: segWidth }; | ||
| 155 | + cumulative += segWidth; | ||
| 156 | + return segment; | ||
| 83 | }); | 157 | }); |
| 84 | - return row; | ||
| 85 | - }); | ||
| 86 | - | ||
| 87 | - // Crear el stack | ||
| 88 | - const stack = d3.stack() | ||
| 89 | - .keys(categories) | ||
| 90 | - .offset(d3.stackOffsetWiggle) | ||
| 91 | - .order(d3.stackOrderInsideOut); | ||
| 92 | 158 | ||
| 93 | - const series = stack(stackData); | 159 | + const chartColors = getChartColors(); |
| 94 | - | 160 | + const colorDefault = chartColors.barDefault; |
| 95 | - // Agregar descripciones a las series | 161 | + const colorHover = chartColors.barHighlight; |
| 96 | - series.forEach(s => { | 162 | + const colorFirst = chartColors.barHighlight; |
| 97 | - const item = filtered.find(d => d.codigo === +s.key); | 163 | + |
| 98 | - s.descripcion = item ? item.descripcion : s.key; | 164 | + const bars = svg.selectAll('rect') |
| 165 | + .data(segments) | ||
| 166 | + .enter() | ||
| 167 | + .append('rect') | ||
| 168 | + .attr('x', d => d.x) | ||
| 169 | + .attr('y', 2) | ||
| 170 | + .attr('width', d => Math.max(d.width - 1, 1)) | ||
| 171 | + .attr('height', height - 4) | ||
| 172 | + .attr('rx', (d, i) => { | ||
| 173 | + if (i === 0) return radius; | ||
| 174 | + if (i === segments.length - 1) return radius; | ||
| 175 | + return 0; | ||
| 176 | + }) | ||
| 177 | + .attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault) | ||
| 178 | + .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault) | ||
| 179 | + .attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5) | ||
| 180 | + .style('cursor', 'pointer') | ||
| 181 | + .on('mouseenter', function(event, d) { | ||
| 182 | + bars | ||
| 183 | + .attr('fill', colorDefault) | ||
| 184 | + .attr('stroke', chartColors.barStrokeDefault) | ||
| 185 | + .attr('stroke-width', 0.5); | ||
| 186 | + d3.select(this) | ||
| 187 | + .attr('fill', colorHover) | ||
| 188 | + .attr('stroke', chartColors.barStroke) | ||
| 189 | + .attr('stroke-width', 1); | ||
| 190 | + hoverData[key] = d; | ||
| 191 | + hoverData = { ...hoverData }; | ||
| 192 | + }) | ||
| 193 | + .on('mouseleave', function() { | ||
| 194 | + bars | ||
| 195 | + .attr('fill', (d, i) => i === 0 ? colorFirst : colorDefault) | ||
| 196 | + .attr('stroke', (d, i) => i === 0 ? chartColors.barStroke : chartColors.barStrokeDefault) | ||
| 197 | + .attr('stroke-width', (d, i) => i === 0 ? 1 : 0.5); | ||
| 198 | + hoverData[key] = null; | ||
| 199 | + hoverData = { ...hoverData }; | ||
| 99 | }); | 200 | }); |
| 100 | - | ||
| 101 | - return { years, series, keys: categories, stackData }; | ||
| 102 | - } | ||
| 103 | - | ||
| 104 | - // Formatear moneda | ||
| 105 | - function formatMoney(value) { | ||
| 106 | - if (value >= 1e9) return `Bs ${(value / 1e9).toFixed(1)}B`; | ||
| 107 | - if (value >= 1e6) return `Bs ${(value / 1e6).toFixed(1)}M`; | ||
| 108 | - if (value >= 1e3) return `Bs ${(value / 1e3).toFixed(0)}K`; | ||
| 109 | - return `Bs ${value.toFixed(0)}`; | ||
| 110 | } | 201 | } |
| 111 | 202 | ||
| 112 | - // Manejar clic para drilldown | 203 | + // Item a mostrar (hover o primero) |
| 113 | - function handleClick(codigo, descripcion) { | 204 | + function getDisplayItem(key, data) { |
| 114 | - const levels = ['tipo', 'clase', 'cuenta', 'subcuenta']; | 205 | + const hover = hoverData[key]; |
| 115 | - const currentIndex = levels.indexOf(currentLevel); | 206 | + if (hover) return hover; |
| 116 | - | 207 | + return data[0] || null; |
| 117 | - if (currentIndex < levels.length - 1) { | 208 | + } |
| 118 | - const nextLevel = levels[currentIndex + 1]; | 209 | + |
| 119 | - | 210 | + // Renderizar gráficos de historia |
| 120 | - // Verificar si hay datos en el siguiente nivel | 211 | + function renderizarHistoria() { |
| 121 | - const hasChildren = streamData.some( | 212 | + if (chartIngresosContainer) { |
| 122 | - d => d.nivel === nextLevel && d.codigo_padre === codigo | 213 | + chartIngresosContainer.innerHTML = ''; |
| 123 | - ); | 214 | + const plot = Plot.plot({ |
| 124 | - | 215 | + width: chartIngresosContainer.clientWidth, |
| 125 | - if (hasChildren) { | 216 | + height: 180, |
| 126 | - currentParent = codigo; | 217 | + marginLeft: 50, |
| 127 | - currentLevel = nextLevel; | 218 | + marginRight: 20, |
| 128 | - breadcrumb = [...breadcrumb, { level: nextLevel, codigo, descripcion }]; | 219 | + marginTop: 20, |
| 129 | - } | 220 | + marginBottom: 30, |
| 130 | - } | 221 | + style: { background: 'transparent', fontFamily: 'var(--font-sans)' }, |
| 131 | - } | 222 | + x: { label: null, tickFormat: d => d }, |
| 132 | - | 223 | + y: { label: null, tickFormat: d => formatearMontoCorto(d), grid: true }, |
| 133 | - // Navegar con breadcrumb | 224 | + marks: [ |
| 134 | - function navigateTo(index) { | 225 | + Plot.areaY(historiaIngresos, { x: 'gestion', y: 'monto', fill: '#4A8B6E', fillOpacity: 0.3 }), |
| 135 | - if (index === 0) { | 226 | + Plot.lineY(historiaIngresos, { x: 'gestion', y: 'monto', stroke: '#4A8B6E', strokeWidth: 2 }), |
| 136 | - currentLevel = 'tipo'; | 227 | + Plot.dot(historiaIngresos, { x: 'gestion', y: 'monto', fill: '#4A8B6E', r: 3 }) |
| 137 | - currentParent = null; | 228 | + ] |
| 138 | - breadcrumb = [{ level: 'tipo', codigo: null, descripcion: 'Todos los ingresos' }]; | 229 | + }); |
| 139 | - } else { | 230 | + chartIngresosContainer.appendChild(plot); |
| 140 | - const target = breadcrumb[index]; | 231 | + } |
| 141 | - currentLevel = target.level; | 232 | + |
| 142 | - currentParent = index > 0 ? breadcrumb[index - 1].codigo : null; | 233 | + if (chartGastosContainer) { |
| 143 | - | 234 | + chartGastosContainer.innerHTML = ''; |
| 144 | - // Si navegamos a un nivel intermedio, necesitamos el padre correcto | 235 | + const plot = Plot.plot({ |
| 145 | - if (index > 0) { | 236 | + width: chartGastosContainer.clientWidth, |
| 146 | - currentParent = breadcrumb[index].codigo; | 237 | + height: 180, |
| 147 | - // Buscar el nivel siguiente | 238 | + marginLeft: 50, |
| 148 | - const levels = ['tipo', 'clase', 'cuenta', 'subcuenta']; | 239 | + marginRight: 20, |
| 149 | - const nextLevelIndex = levels.indexOf(target.level) + 1; | 240 | + marginTop: 20, |
| 150 | - if (nextLevelIndex < levels.length) { | 241 | + marginBottom: 30, |
| 151 | - currentLevel = levels[nextLevelIndex]; | 242 | + style: { background: 'transparent', fontFamily: 'var(--font-sans)' }, |
| 152 | - } | 243 | + x: { label: null, tickFormat: d => d }, |
| 153 | - } | 244 | + y: { label: null, tickFormat: d => formatearMontoCorto(d), grid: true }, |
| 154 | - | 245 | + marks: [ |
| 155 | - breadcrumb = breadcrumb.slice(0, index + 1); | 246 | + Plot.areaY(historiaGastos, { x: 'gestion', y: 'monto', fill: '#C9A751', fillOpacity: 0.3 }), |
| 247 | + Plot.lineY(historiaGastos, { x: 'gestion', y: 'monto', stroke: '#C9A751', strokeWidth: 2 }), | ||
| 248 | + Plot.dot(historiaGastos, { x: 'gestion', y: 'monto', fill: '#C9A751', r: 3 }) | ||
| 249 | + ] | ||
| 250 | + }); | ||
| 251 | + chartGastosContainer.appendChild(plot); | ||
| 156 | } | 252 | } |
| 157 | } | 253 | } |
| 158 | 254 | ||
| 159 | - // Efecto para observar tamaño | 255 | + // Efectos |
| 160 | $effect(() => { | 256 | $effect(() => { |
| 161 | - if (!container) return; | 257 | + setTimeout(() => { |
| 162 | - | 258 | + clasificadoresGasto.forEach(({ key, data }) => { |
| 163 | - const observer = new ResizeObserver(entries => { | 259 | + if (barContainers[key]) renderizarBarras(key, data); |
| 164 | - for (const entry of entries) { | 260 | + }); |
| 165 | - width = entry.contentRect.width; | 261 | + clasificadoresIngreso.forEach(({ key, data }) => { |
| 166 | - } | 262 | + if (barContainers[key]) renderizarBarras(key, data); |
| 263 | + }); | ||
| 264 | + }, 100); | ||
| 167 | }); | 265 | }); |
| 168 | 266 | ||
| 169 | - observer.observe(container); | 267 | + $effect(() => { |
| 170 | - return () => observer.disconnect(); | 268 | + if (chartIngresosContainer && chartGastosContainer) { |
| 269 | + setTimeout(renderizarHistoria, 150); | ||
| 270 | + } | ||
| 171 | }); | 271 | }); |
| 172 | 272 | ||
| 173 | onMount(() => { | 273 | onMount(() => { |
| 174 | - loadData(); | 274 | + // Observer para tema |
| 275 | + const observer = new MutationObserver(() => { | ||
| 276 | + clasificadoresGasto.forEach(({ key, data }) => { | ||
| 277 | + if (barContainers[key]) renderizarBarras(key, data); | ||
| 175 | }); | 278 | }); |
| 279 | + clasificadoresIngreso.forEach(({ key, data }) => { | ||
| 280 | + if (barContainers[key]) renderizarBarras(key, data); | ||
| 281 | + }); | ||
| 282 | + }); | ||
| 283 | + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); | ||
| 176 | 284 | ||
| 177 | - // Datos preparados reactivos | 285 | + // Resize observer para charts |
| 178 | - let prepared = $derived(prepareStreamData()); | 286 | + const resizeObserver = new ResizeObserver(() => { |
| 179 | - | 287 | + renderizarHistoria(); |
| 180 | - // Escalas reactivas | ||
| 181 | - let xScale = $derived( | ||
| 182 | - d3.scaleLinear() | ||
| 183 | - .domain(d3.extent(prepared.years)) | ||
| 184 | - .range([60, width - 20]) | ||
| 185 | - ); | ||
| 186 | - | ||
| 187 | - let yExtent = $derived(() => { | ||
| 188 | - if (!prepared.series.length) return [0, 0]; | ||
| 189 | - const allValues = prepared.series.flatMap(s => s.flatMap(d => [d[0], d[1]])); | ||
| 190 | - return d3.extent(allValues); | ||
| 191 | }); | 288 | }); |
| 289 | + if (chartIngresosContainer) resizeObserver.observe(chartIngresosContainer); | ||
| 290 | + if (chartGastosContainer) resizeObserver.observe(chartGastosContainer); | ||
| 192 | 291 | ||
| 193 | - let yScale = $derived( | 292 | + return () => { |
| 194 | - d3.scaleLinear() | 293 | + observer.disconnect(); |
| 195 | - .domain(yExtent()) | 294 | + resizeObserver.disconnect(); |
| 196 | - .range([height - 40, 40]) | 295 | + }; |
| 197 | - ); | 296 | + }); |
| 198 | - | ||
| 199 | - // Generador de área | ||
| 200 | - let areaGenerator = $derived( | ||
| 201 | - d3.area() | ||
| 202 | - .x(d => xScale(d.data.año)) | ||
| 203 | - .y0(d => yScale(d[0])) | ||
| 204 | - .y1(d => yScale(d[1])) | ||
| 205 | - .curve(d3.curveBasis) | ||
| 206 | - ); | ||
| 207 | </script> | 297 | </script> |
| 208 | 298 | ||
| 209 | <svelte:head> | 299 | <svelte:head> |
| 210 | - <title>Streamgraph - Ingresos del Estado</title> | 300 | + <title>{entidad.nombre} - Radiografía</title> |
| 211 | </svelte:head> | 301 | </svelte:head> |
| 212 | 302 | ||
| 213 | -<div class="page"> | 303 | +<div class="dashboard"> |
| 214 | - <header> | 304 | + <!-- Header --> |
| 215 | - <h1>Ingresos del Estado Boliviano</h1> | 305 | + <header class="dashboard-header"> |
| 216 | - <p class="subtitle">Streamgraph interactivo con drilldown · 2005-2025</p> | 306 | + <div class="header-info"> |
| 307 | + <h1 class="entidad-nombre">{entidad.nombre}</h1> | ||
| 308 | + <div class="entidad-meta"> | ||
| 309 | + <span class="entidad-sigla">{entidad.sigla}</span> | ||
| 310 | + <span class="meta-sep">·</span> | ||
| 311 | + <span class="entidad-sector">{entidad.sector}</span> | ||
| 312 | + </div> | ||
| 313 | + </div> | ||
| 314 | + <div class="header-control"> | ||
| 315 | + <label class="gestion-label">Gestión</label> | ||
| 316 | + <select class="gestion-select" bind:value={gestionSeleccionada}> | ||
| 317 | + {#each gestiones as g} | ||
| 318 | + <option value={g}>{g}</option> | ||
| 319 | + {/each} | ||
| 320 | + </select> | ||
| 321 | + </div> | ||
| 217 | </header> | 322 | </header> |
| 218 | 323 | ||
| 219 | - <!-- Breadcrumb --> | 324 | + <!-- Ranking --> |
| 220 | - <nav class="breadcrumb"> | 325 | + <div class="ranking-bar"> |
| 221 | - {#each breadcrumb as crumb, i} | 326 | + <div class="ranking-item"> |
| 222 | - {#if i > 0} | 327 | + <span class="ranking-label">Ranking gastos</span> |
| 223 | - <span class="separator">→</span> | 328 | + <span class="ranking-value">#{ranking.gastos.posicion}</span> |
| 224 | - {/if} | 329 | + <span class="ranking-total">de {ranking.gastos.total}</span> |
| 225 | - <button | 330 | + </div> |
| 226 | - class="crumb" | 331 | + <div class="ranking-sep"></div> |
| 227 | - class:active={i === breadcrumb.length - 1} | 332 | + <div class="ranking-item"> |
| 228 | - onclick={() => navigateTo(i)} | 333 | + <span class="ranking-label">Ranking ingresos</span> |
| 229 | - disabled={i === breadcrumb.length - 1} | 334 | + <span class="ranking-value">#{ranking.ingresos.posicion}</span> |
| 230 | - > | 335 | + <span class="ranking-total">de {ranking.ingresos.total}</span> |
| 231 | - {crumb.descripcion} | 336 | + </div> |
| 232 | - </button> | ||
| 233 | - {/each} | ||
| 234 | - </nav> | ||
| 235 | - | ||
| 236 | - <!-- Nivel actual --> | ||
| 237 | - <div class="level-indicator"> | ||
| 238 | - Nivel: <strong>{currentLevel}</strong> | ||
| 239 | - {#if currentLevel !== 'subcuenta'} | ||
| 240 | - <span class="hint">· Clic en una banda para ver detalle</span> | ||
| 241 | - {/if} | ||
| 242 | </div> | 337 | </div> |
| 243 | 338 | ||
| 244 | - <!-- Contenedor del gráfico --> | 339 | + <!-- Historia temporal --> |
| 245 | - <div class="chart-container" bind:this={container}> | 340 | + <section class="seccion"> |
| 246 | - {#if loading} | 341 | + <h2 class="seccion-titulo">Historia 2005 – 2025</h2> |
| 247 | - <div class="loading">Cargando datos...</div> | 342 | + <div class="historia-grid"> |
| 248 | - {:else if width > 0 && prepared.series.length > 0} | 343 | + <div class="historia-card"> |
| 249 | - <svg {width} {height}> | 344 | + <div class="historia-header"> |
| 250 | - <!-- Eje X --> | 345 | + <span class="historia-label">Ingresos</span> |
| 251 | - <g class="axis-x" transform="translate(0, {height - 35})"> | 346 | + <span class="historia-monto ingresos">{formatearMonto(historiaIngresos[historiaIngresos.length - 1].monto)} Bs</span> |
| 252 | - {#each prepared.years.filter((_, i) => i % 2 === 0) as year} | 347 | + </div> |
| 253 | - <text x={xScale(year)} y="20" text-anchor="middle" class="axis-label"> | 348 | + <div class="historia-chart" bind:this={chartIngresosContainer}></div> |
| 254 | - {year} | 349 | + </div> |
| 255 | - </text> | 350 | + <div class="historia-card"> |
| 256 | - <line | 351 | + <div class="historia-header"> |
| 257 | - x1={xScale(year)} | 352 | + <span class="historia-label">Gastos</span> |
| 258 | - y1="-{height - 80}" | 353 | + <span class="historia-monto gastos">{formatearMonto(historiaGastos[historiaGastos.length - 1].monto)} Bs</span> |
| 259 | - x2={xScale(year)} | 354 | + </div> |
| 260 | - y2="0" | 355 | + <div class="historia-chart" bind:this={chartGastosContainer}></div> |
| 261 | - class="grid-line" | 356 | + </div> |
| 262 | - /> | 357 | + </div> |
| 263 | - {/each} | 358 | + </section> |
| 264 | - </g> | 359 | + |
| 265 | - | 360 | + <!-- Composición del gasto --> |
| 266 | - <!-- Streams --> | 361 | + <section class="seccion"> |
| 267 | - {#each prepared.series as serie, i} | 362 | + <h2 class="seccion-titulo">¿En qué gasta? <span class="gestion-badge">{gestionSeleccionada}</span></h2> |
| 268 | - {@const color = colorScale(serie.key)} | 363 | + <div class="clasificadores-grid"> |
| 269 | - <path | 364 | + {#each clasificadoresGasto as { key, label, data }} |
| 270 | - d={areaGenerator(serie)} | 365 | + {@const displayItem = getDisplayItem(key, data)} |
| 271 | - fill={color} | 366 | + {@const total = getTotal(data)} |
| 272 | - fill-opacity="0.85" | 367 | + <div class="clasificador-card"> |
| 273 | - stroke={color} | 368 | + <div class="clasificador-header"> |
| 274 | - stroke-width="0.5" | 369 | + <div class="clasificador-info"> |
| 275 | - class="stream-path" | 370 | + <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} Bs</span> |
| 276 | - class:clickable={currentLevel !== 'subcuenta'} | 371 | + <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> |
| 277 | - onmouseenter={(e) => { | 372 | + <span class="clasificador-padre">({displayItem?.padre || ''})</span> |
| 278 | - tooltip = { | 373 | + </div> |
| 279 | - show: true, | 374 | + <span class="clasificador-label">{label}</span> |
| 280 | - x: e.clientX, | 375 | + </div> |
| 281 | - y: e.clientY, | 376 | + <div class="barra-container" bind:this={barContainers[key]}></div> |
| 282 | - data: { | 377 | + </div> |
| 283 | - codigo: serie.key, | ||
| 284 | - descripcion: serie.descripcion, | ||
| 285 | - color, | ||
| 286 | - total: d3.sum(serie, d => d[1] - d[0]) | ||
| 287 | - } | ||
| 288 | - }; | ||
| 289 | - }} | ||
| 290 | - onmousemove={(e) => { | ||
| 291 | - tooltip.x = e.clientX; | ||
| 292 | - tooltip.y = e.clientY; | ||
| 293 | - }} | ||
| 294 | - onmouseleave={() => tooltip.show = false} | ||
| 295 | - onclick={() => handleClick(+serie.key, serie.descripcion)} | ||
| 296 | - /> | ||
| 297 | - {/each} | ||
| 298 | - </svg> | ||
| 299 | - | ||
| 300 | - <!-- Leyenda --> | ||
| 301 | - <div class="legend"> | ||
| 302 | - {#each prepared.series as serie} | ||
| 303 | - {@const color = colorScale(serie.key)} | ||
| 304 | - <button | ||
| 305 | - class="legend-item" | ||
| 306 | - onclick={() => handleClick(+serie.key, serie.descripcion)} | ||
| 307 | - > | ||
| 308 | - <span class="legend-color" style="background: {color}"></span> | ||
| 309 | - <span class="legend-label">{serie.descripcion}</span> | ||
| 310 | - </button> | ||
| 311 | {/each} | 378 | {/each} |
| 312 | </div> | 379 | </div> |
| 313 | - {:else} | 380 | + </section> |
| 314 | - <div class="no-data">No hay datos para este nivel</div> | 381 | + |
| 315 | - {/if} | 382 | + <!-- Origen del dinero --> |
| 383 | + <section class="seccion"> | ||
| 384 | + <h2 class="seccion-titulo">¿De dónde viene la plata? <span class="gestion-badge">{gestionSeleccionada}</span></h2> | ||
| 385 | + <div class="clasificadores-grid"> | ||
| 386 | + {#each clasificadoresIngreso as { key, label, data }} | ||
| 387 | + {@const displayItem = getDisplayItem(key, data)} | ||
| 388 | + {@const total = getTotal(data)} | ||
| 389 | + <div class="clasificador-card"> | ||
| 390 | + <div class="clasificador-header"> | ||
| 391 | + <div class="clasificador-info"> | ||
| 392 | + <span class="clasificador-monto">{formatearMonto(displayItem?.monto || 0)} Bs</span> | ||
| 393 | + <span class="clasificador-nombre">{displayItem?.nombre || ''}</span> | ||
| 394 | + <span class="clasificador-padre">({displayItem?.padre || ''})</span> | ||
| 316 | </div> | 395 | </div> |
| 317 | - | 396 | + <span class="clasificador-label">{label}</span> |
| 318 | - <!-- Tooltip --> | ||
| 319 | - {#if tooltip.show && tooltip.data} | ||
| 320 | - <div | ||
| 321 | - class="tooltip" | ||
| 322 | - style="left: {tooltip.x + 15}px; top: {tooltip.y - 10}px;" | ||
| 323 | - > | ||
| 324 | - <div class="tooltip-header" style="border-color: {tooltip.data.color}"> | ||
| 325 | - <span class="tooltip-code">{tooltip.data.codigo}</span> | ||
| 326 | - <span class="tooltip-desc">{tooltip.data.descripcion}</span> | ||
| 327 | </div> | 397 | </div> |
| 328 | - <div class="tooltip-value"> | 398 | + <div class="barra-container" bind:this={barContainers[key]}></div> |
| 329 | - Total acumulado: {formatMoney(tooltip.data.total)} | ||
| 330 | </div> | 399 | </div> |
| 331 | - {#if currentLevel !== 'subcuenta'} | 400 | + {/each} |
| 332 | - <div class="tooltip-hint">Clic para ver detalle</div> | ||
| 333 | - {/if} | ||
| 334 | </div> | 401 | </div> |
| 335 | - {/if} | 402 | + </section> |
| 336 | </div> | 403 | </div> |
| 337 | 404 | ||
| 338 | <style> | 405 | <style> |
| 339 | - .page { | 406 | + .dashboard { |
| 340 | min-height: 100vh; | 407 | min-height: 100vh; |
| 341 | - background: #1a1a1a; | 408 | + background: var(--theme-fondo); |
| 342 | - color: #f5f5f5; | 409 | + color: var(--theme-texto); |
| 343 | padding: 2rem; | 410 | padding: 2rem; |
| 344 | - font-family: system-ui, -apple-system, sans-serif; | 411 | + font-family: var(--font-sans); |
| 345 | } | 412 | } |
| 346 | 413 | ||
| 347 | - header { | 414 | + /* Header */ |
| 348 | - text-align: center; | 415 | + .dashboard-header { |
| 349 | - margin-bottom: 2rem; | 416 | + display: flex; |
| 417 | + justify-content: space-between; | ||
| 418 | + align-items: flex-start; | ||
| 419 | + gap: 2rem; | ||
| 420 | + margin-bottom: 1.5rem; | ||
| 421 | + padding-bottom: 1.5rem; | ||
| 422 | + border-bottom: 1px solid var(--theme-borde); | ||
| 350 | } | 423 | } |
| 351 | 424 | ||
| 352 | - h1 { | 425 | + .entidad-nombre { |
| 353 | - font-size: 2rem; | 426 | + font-size: 1.75rem; |
| 354 | font-weight: 700; | 427 | font-weight: 700; |
| 355 | - margin: 0; | 428 | + color: var(--theme-titulo); |
| 356 | - color: #f5f5f5; | 429 | + margin: 0 0 0.5rem 0; |
| 430 | + line-height: 1.2; | ||
| 431 | + } | ||
| 432 | + | ||
| 433 | + .entidad-meta { | ||
| 434 | + display: flex; | ||
| 435 | + align-items: center; | ||
| 436 | + gap: 0.5rem; | ||
| 437 | + font-size: 0.9rem; | ||
| 438 | + color: var(--theme-texto); | ||
| 439 | + opacity: 0.7; | ||
| 440 | + } | ||
| 441 | + | ||
| 442 | + .entidad-sigla { | ||
| 443 | + font-weight: 600; | ||
| 444 | + color: var(--theme-titulo); | ||
| 445 | + opacity: 1; | ||
| 446 | + } | ||
| 447 | + | ||
| 448 | + .meta-sep { | ||
| 449 | + opacity: 0.4; | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + .header-control { | ||
| 453 | + display: flex; | ||
| 454 | + flex-direction: column; | ||
| 455 | + gap: 0.25rem; | ||
| 357 | } | 456 | } |
| 358 | 457 | ||
| 359 | - .subtitle { | 458 | + .gestion-label { |
| 360 | - color: #888; | 459 | + font-size: 0.7rem; |
| 361 | - margin-top: 0.5rem; | 460 | + text-transform: uppercase; |
| 461 | + letter-spacing: 0.05em; | ||
| 462 | + color: var(--theme-texto); | ||
| 463 | + opacity: 0.6; | ||
| 362 | } | 464 | } |
| 363 | 465 | ||
| 364 | - .breadcrumb { | 466 | + .gestion-select { |
| 467 | + padding: 0.5rem 1rem; | ||
| 468 | + font-size: 0.95rem; | ||
| 469 | + font-weight: 600; | ||
| 470 | + background: var(--theme-tarjeta); | ||
| 471 | + border: 1px solid var(--theme-borde); | ||
| 472 | + border-radius: 8px; | ||
| 473 | + color: var(--theme-titulo); | ||
| 474 | + cursor: pointer; | ||
| 475 | + } | ||
| 476 | + | ||
| 477 | + /* Ranking */ | ||
| 478 | + .ranking-bar { | ||
| 365 | display: flex; | 479 | display: flex; |
| 366 | align-items: center; | 480 | align-items: center; |
| 481 | + gap: 1.5rem; | ||
| 482 | + padding: 1rem 1.5rem; | ||
| 483 | + background: var(--theme-tarjeta); | ||
| 484 | + border-radius: 12px; | ||
| 485 | + margin-bottom: 2rem; | ||
| 486 | + } | ||
| 487 | + | ||
| 488 | + .ranking-item { | ||
| 489 | + display: flex; | ||
| 490 | + align-items: baseline; | ||
| 367 | gap: 0.5rem; | 491 | gap: 0.5rem; |
| 368 | - margin-bottom: 1rem; | ||
| 369 | - flex-wrap: wrap; | ||
| 370 | } | 492 | } |
| 371 | 493 | ||
| 372 | - .separator { | 494 | + .ranking-label { |
| 373 | - color: #555; | 495 | + font-size: 0.8rem; |
| 496 | + color: var(--theme-texto); | ||
| 497 | + opacity: 0.7; | ||
| 374 | } | 498 | } |
| 375 | 499 | ||
| 376 | - .crumb { | 500 | + .ranking-value { |
| 377 | - background: #2a2a2a; | 501 | + font-size: 1.25rem; |
| 378 | - border: 1px solid #333; | 502 | + font-weight: 700; |
| 379 | - color: #aaa; | 503 | + color: #C9A751; |
| 380 | - padding: 0.4rem 0.8rem; | ||
| 381 | - border-radius: 6px; | ||
| 382 | - cursor: pointer; | ||
| 383 | - font-size: 0.85rem; | ||
| 384 | - transition: all 0.2s; | ||
| 385 | } | 504 | } |
| 386 | 505 | ||
| 387 | - .crumb:hover:not(:disabled) { | 506 | + .ranking-total { |
| 388 | - background: #333; | 507 | + font-size: 0.8rem; |
| 389 | - color: #fff; | 508 | + color: var(--theme-texto); |
| 509 | + opacity: 0.5; | ||
| 390 | } | 510 | } |
| 391 | 511 | ||
| 392 | - .crumb.active { | 512 | + .ranking-sep { |
| 393 | - background: #C9A751; | 513 | + width: 1px; |
| 394 | - color: #1a1a1a; | 514 | + height: 24px; |
| 395 | - font-weight: 600; | 515 | + background: var(--theme-borde); |
| 396 | - cursor: default; | ||
| 397 | } | 516 | } |
| 398 | 517 | ||
| 399 | - .crumb:disabled { | 518 | + /* Secciones */ |
| 400 | - cursor: default; | 519 | + .seccion { |
| 520 | + margin-bottom: 2.5rem; | ||
| 401 | } | 521 | } |
| 402 | 522 | ||
| 403 | - .level-indicator { | 523 | + .seccion-titulo { |
| 404 | - color: #666; | 524 | + font-size: 1.1rem; |
| 405 | - font-size: 0.85rem; | 525 | + font-weight: 600; |
| 406 | - margin-bottom: 1rem; | 526 | + color: var(--theme-titulo); |
| 527 | + margin: 0 0 1rem 0; | ||
| 528 | + display: flex; | ||
| 529 | + align-items: center; | ||
| 530 | + gap: 0.75rem; | ||
| 407 | } | 531 | } |
| 408 | 532 | ||
| 409 | - .level-indicator strong { | 533 | + .gestion-badge { |
| 534 | + font-size: 0.75rem; | ||
| 535 | + font-weight: 500; | ||
| 536 | + padding: 0.25rem 0.6rem; | ||
| 537 | + background: rgba(201, 167, 81, 0.15); | ||
| 410 | color: #C9A751; | 538 | color: #C9A751; |
| 411 | - text-transform: uppercase; | 539 | + border-radius: 4px; |
| 412 | } | 540 | } |
| 413 | 541 | ||
| 414 | - .hint { | 542 | + /* Historia */ |
| 415 | - color: #555; | 543 | + .historia-grid { |
| 544 | + display: grid; | ||
| 545 | + grid-template-columns: repeat(2, 1fr); | ||
| 546 | + gap: 1.5rem; | ||
| 416 | } | 547 | } |
| 417 | 548 | ||
| 418 | - .chart-container { | 549 | + .historia-card { |
| 419 | - background: #222; | 550 | + background: var(--theme-tarjeta); |
| 420 | border-radius: 12px; | 551 | border-radius: 12px; |
| 421 | padding: 1rem; | 552 | padding: 1rem; |
| 422 | - min-height: 500px; | ||
| 423 | - position: relative; | ||
| 424 | } | 553 | } |
| 425 | 554 | ||
| 426 | - .loading, .no-data { | 555 | + .historia-header { |
| 427 | display: flex; | 556 | display: flex; |
| 428 | - align-items: center; | 557 | + justify-content: space-between; |
| 429 | - justify-content: center; | 558 | + align-items: baseline; |
| 430 | - height: 400px; | 559 | + margin-bottom: 0.75rem; |
| 431 | - color: #666; | ||
| 432 | } | 560 | } |
| 433 | 561 | ||
| 434 | - svg { | 562 | + .historia-label { |
| 435 | - display: block; | 563 | + font-size: 0.85rem; |
| 564 | + font-weight: 500; | ||
| 565 | + color: var(--theme-texto); | ||
| 436 | } | 566 | } |
| 437 | 567 | ||
| 438 | - .axis-label { | 568 | + .historia-monto { |
| 439 | - fill: #666; | 569 | + font-size: 0.95rem; |
| 440 | - font-size: 12px; | 570 | + font-weight: 600; |
| 441 | } | 571 | } |
| 442 | 572 | ||
| 443 | - .grid-line { | 573 | + .historia-monto.ingresos { |
| 444 | - stroke: #333; | 574 | + color: #4A8B6E; |
| 445 | - stroke-width: 0.5; | ||
| 446 | - stroke-dasharray: 2, 4; | ||
| 447 | } | 575 | } |
| 448 | 576 | ||
| 449 | - .stream-path { | 577 | + .historia-monto.gastos { |
| 450 | - transition: fill-opacity 0.2s, transform 0.2s; | 578 | + color: #C9A751; |
| 451 | } | 579 | } |
| 452 | 580 | ||
| 453 | - .stream-path:hover { | 581 | + .historia-chart { |
| 454 | - fill-opacity: 1; | 582 | + width: 100%; |
| 583 | + height: 180px; | ||
| 455 | } | 584 | } |
| 456 | 585 | ||
| 457 | - .stream-path.clickable { | 586 | + /* Clasificadores */ |
| 458 | - cursor: pointer; | 587 | + .clasificadores-grid { |
| 588 | + display: flex; | ||
| 589 | + flex-direction: column; | ||
| 590 | + gap: 1rem; | ||
| 459 | } | 591 | } |
| 460 | 592 | ||
| 461 | - .stream-path.clickable:hover { | 593 | + .clasificador-card { |
| 462 | - filter: brightness(1.1); | 594 | + display: flex; |
| 595 | + flex-direction: column; | ||
| 596 | + gap: 0.35rem; | ||
| 463 | } | 597 | } |
| 464 | 598 | ||
| 465 | - .legend { | 599 | + .clasificador-header { |
| 466 | display: flex; | 600 | display: flex; |
| 467 | - flex-wrap: wrap; | 601 | + align-items: baseline; |
| 468 | - gap: 0.5rem; | 602 | + justify-content: space-between; |
| 469 | - margin-top: 1rem; | 603 | + gap: 1rem; |
| 470 | - padding-top: 1rem; | ||
| 471 | - border-top: 1px solid #333; | ||
| 472 | } | 604 | } |
| 473 | 605 | ||
| 474 | - .legend-item { | 606 | + .clasificador-info { |
| 475 | display: flex; | 607 | display: flex; |
| 476 | - align-items: center; | 608 | + align-items: baseline; |
| 477 | gap: 0.4rem; | 609 | gap: 0.4rem; |
| 478 | - background: #2a2a2a; | 610 | + flex-wrap: wrap; |
| 479 | - border: 1px solid #333; | ||
| 480 | - padding: 0.3rem 0.6rem; | ||
| 481 | - border-radius: 4px; | ||
| 482 | - cursor: pointer; | ||
| 483 | - transition: all 0.2s; | ||
| 484 | } | 611 | } |
| 485 | 612 | ||
| 486 | - .legend-item:hover { | 613 | + .clasificador-monto { |
| 487 | - background: #333; | 614 | + font-size: 0.95rem; |
| 615 | + font-weight: 600; | ||
| 616 | + color: var(--theme-titulo); | ||
| 488 | } | 617 | } |
| 489 | 618 | ||
| 490 | - .legend-color { | 619 | + .clasificador-nombre { |
| 491 | - width: 12px; | 620 | + font-size: 0.85rem; |
| 492 | - height: 12px; | 621 | + font-weight: 500; |
| 493 | - border-radius: 2px; | 622 | + color: var(--theme-titulo); |
| 623 | + } | ||
| 624 | + | ||
| 625 | + .clasificador-padre { | ||
| 626 | + font-size: 0.7rem; | ||
| 627 | + color: var(--theme-texto); | ||
| 628 | + opacity: 0.5; | ||
| 629 | + } | ||
| 630 | + | ||
| 631 | + .clasificador-label { | ||
| 632 | + font-size: 0.65rem; | ||
| 633 | + text-transform: uppercase; | ||
| 634 | + letter-spacing: 0.05em; | ||
| 635 | + color: var(--theme-texto); | ||
| 636 | + opacity: 0.6; | ||
| 494 | flex-shrink: 0; | 637 | flex-shrink: 0; |
| 495 | } | 638 | } |
| 496 | 639 | ||
| 497 | - .legend-label { | 640 | + .barra-container { |
| 498 | - font-size: 0.75rem; | 641 | + width: 100%; |
| 499 | - color: #aaa; | 642 | + height: 48px; |
| 500 | - max-width: 150px; | 643 | + border-radius: 4px; |
| 501 | overflow: hidden; | 644 | overflow: hidden; |
| 502 | - text-overflow: ellipsis; | ||
| 503 | - white-space: nowrap; | ||
| 504 | } | 645 | } |
| 505 | 646 | ||
| 506 | - .tooltip { | 647 | + /* Responsive */ |
| 507 | - position: fixed; | 648 | + @media (max-width: 768px) { |
| 508 | - background: rgba(30, 30, 30, 0.95); | 649 | + .dashboard { |
| 509 | - backdrop-filter: blur(8px); | 650 | + padding: 1rem; |
| 510 | - border: 1px solid #444; | ||
| 511 | - border-radius: 8px; | ||
| 512 | - padding: 0.75rem 1rem; | ||
| 513 | - pointer-events: none; | ||
| 514 | - z-index: 1000; | ||
| 515 | - max-width: 300px; | ||
| 516 | - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); | ||
| 517 | } | 651 | } |
| 518 | 652 | ||
| 519 | - .tooltip-header { | 653 | + .dashboard-header { |
| 520 | - border-left: 3px solid; | 654 | + flex-direction: column; |
| 521 | - padding-left: 0.5rem; | 655 | + gap: 1rem; |
| 522 | - margin-bottom: 0.5rem; | ||
| 523 | } | 656 | } |
| 524 | 657 | ||
| 525 | - .tooltip-code { | 658 | + .entidad-nombre { |
| 526 | - font-family: monospace; | 659 | + font-size: 1.35rem; |
| 527 | - font-size: 0.75rem; | ||
| 528 | - color: #888; | ||
| 529 | - display: block; | ||
| 530 | } | 660 | } |
| 531 | 661 | ||
| 532 | - .tooltip-desc { | 662 | + .historia-grid { |
| 533 | - font-size: 0.9rem; | 663 | + grid-template-columns: 1fr; |
| 534 | - color: #f5f5f5; | ||
| 535 | - font-weight: 500; | ||
| 536 | } | 664 | } |
| 537 | 665 | ||
| 538 | - .tooltip-value { | 666 | + .ranking-bar { |
| 539 | - font-size: 0.85rem; | 667 | + flex-direction: column; |
| 540 | - color: #C9A751; | 668 | + align-items: flex-start; |
| 541 | - font-weight: 600; | 669 | + gap: 0.75rem; |
| 542 | } | 670 | } |
| 543 | 671 | ||
| 544 | - .tooltip-hint { | 672 | + .ranking-sep { |
| 545 | - font-size: 0.75rem; | 673 | + width: 100%; |
| 546 | - color: #666; | 674 | + height: 1px; |
| 547 | - margin-top: 0.3rem; | 675 | + } |
| 548 | } | 676 | } |
| 549 | </style> | 677 | </style> | ... | ... |
static/ESTILOS_UI.md
deleted
100644 → 0
| 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` | |
tablas.md
0 → 100644
| 1 | +# Estructura de Bases de Datos - Presupuesto Público Bolivia | ||
| 2 | + | ||
| 3 | +Este documento detalla las tablas, vistas y sus estructuras en el esquema `ppto` de Supabase. | ||
| 4 | + | ||
| 5 | +--- | ||
| 6 | + | ||
| 7 | +## Tablas de Clasificadores | ||
| 8 | + | ||
| 9 | +### 1. `clas_institucional` - Clasificador Institucional | ||
| 10 | + | ||
| 11 | +| Columna | Tipo | Descripción | | ||
| 12 | +|---------|------|-------------| | ||
| 13 | +| entidad | INTEGER | Código único de la entidad | | ||
| 14 | +| desc_entidad | TEXT | Nombre completo de la entidad | | ||
| 15 | +| sigla_entidad | VARCHAR(50) | Sigla de la entidad | | ||
| 16 | +| sector | VARCHAR(10) | Código del sector | | ||
| 17 | +| desc_sector | TEXT | Nombre del sector | | ||
| 18 | +| sigla_sector | VARCHAR(20) | Sigla del sector | | ||
| 19 | +| subsector | VARCHAR(10) | Código del subsector | | ||
| 20 | +| desc_subsector | TEXT | Nombre del subsector | | ||
| 21 | +| sigla_subsector | VARCHAR(20) | Sigla del subsector | | ||
| 22 | +| area | VARCHAR(10) | Código del área | | ||
| 23 | +| desc_area | TEXT | Nombre del área | | ||
| 24 | +| sigla_area | VARCHAR(20) | Sigla del área | | ||
| 25 | +| subarea | VARCHAR(10) | Código de la subárea | | ||
| 26 | +| desc_subarea | TEXT | Nombre de la subárea | | ||
| 27 | +| sigla_subarea | VARCHAR(20) | Sigla de la subárea | | ||
| 28 | +| gestiones | TEXT | Lista CSV de años con datos (ej: "2006,2007,...,2025") | | ||
| 29 | +| n_gestiones | INTEGER | Cantidad de años con datos | | ||
| 30 | + | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +--- | ||
| 34 | + | ||
| 35 | +### 2. `clas_objetos` - Clasificador de Objetos de Gasto | ||
| 36 | + | ||
| 37 | +| Columna | Tipo | Descripción | | ||
| 38 | +|---------|------|-------------| | ||
| 39 | +| nivel | TEXT | Nivel jerárquico: 'grupo', 'subgrupo', 'partida', 'subpartida' | | ||
| 40 | +| objeto | TEXT (PK) | Código del objeto (ej: "10000", "11000", "11100") | | ||
| 41 | +| desc_objeto | TEXT | Descripción vigente del objeto de gasto | | ||
| 42 | +| gestiones | TEXT | Rangos de años donde existe (ej: "2005, 2007-2020, 2022-2026") | | ||
| 43 | +| n_gestiones | INTEGER | Cantidad de años con datos | | ||
| 44 | +| n_variaciones | INTEGER | Cantidad de variaciones históricas en la descripción | | ||
| 45 | +| descripciones | JSONB | Historial de descripciones por rangos de años | | ||
| 46 | + | ||
| 47 | +**Estructura de `descripciones`:** | ||
| 48 | +```json | ||
| 49 | +[ | ||
| 50 | + { | ||
| 51 | + "descripcion": "Descripción vigente del objeto...", | ||
| 52 | + "rangos": "2011-2020, 2022-2026" | ||
| 53 | + }, | ||
| 54 | + { | ||
| 55 | + "descripcion": "Descripción anterior del objeto...", | ||
| 56 | + "rangos": "2005, 2007-2010" | ||
| 57 | + } | ||
| 58 | +] | ||
| 59 | +``` | ||
| 60 | + | ||
| 61 | +**Jerarquía derivada del código `objeto`:** | ||
| 62 | +``` | ||
| 63 | +Nivel Patrón Ejemplo | ||
| 64 | +────────────────────────────────── | ||
| 65 | +grupo X0000 10000, 20000 | ||
| 66 | +subgrupo XX000 11000, 12000 | ||
| 67 | +partida XXX00 11100, 11200 | ||
| 68 | +subpartida XXXX0/X 11110, 11111 | ||
| 69 | +``` | ||
| 70 | + | ||
| 71 | +**Sin índices.** La tabla tiene ~500 filas y se carga completa al inicio. | ||
| 72 | + | ||
| 73 | +--- | ||
| 74 | + | ||
| 75 | +### 3. `clas_rubros` - Clasificador de Rubros (Ingresos) | ||
| 76 | + | ||
| 77 | +| Columna | Tipo | Descripción | | ||
| 78 | +|---------|------|-------------| | ||
| 79 | +| nivel | TEXT | Nivel jerárquico: 'tipo', 'clase', 'cuenta', 'sub_cuenta' | | ||
| 80 | +| rubro | TEXT (PK) | Código del rubro (ej: "1000", "1100", "1110") | | ||
| 81 | +| desc_rubro | TEXT | Descripción vigente del rubro | | ||
| 82 | +| gestiones | TEXT | Rangos de años donde existe (ej: "2005, 2007-2020, 2022-2026") | | ||
| 83 | +| n_gestiones | INTEGER | Cantidad de años con datos | | ||
| 84 | +| n_variaciones | INTEGER | Cantidad de variaciones históricas en la descripción | | ||
| 85 | +| descripciones | JSONB | Historial de descripciones por rangos de años | | ||
| 86 | + | ||
| 87 | +**Estructura de `descripciones`:** | ||
| 88 | +```json | ||
| 89 | +[ | ||
| 90 | + { | ||
| 91 | + "descripcion": "Descripción vigente del rubro...", | ||
| 92 | + "rangos": "2011-2020, 2022-2026" | ||
| 93 | + }, | ||
| 94 | + { | ||
| 95 | + "descripcion": "Descripción anterior del rubro...", | ||
| 96 | + "rangos": "2005, 2007-2010" | ||
| 97 | + } | ||
| 98 | +] | ||
| 99 | +``` | ||
| 100 | + | ||
| 101 | +**Jerarquía derivada del código `rubro`:** | ||
| 102 | +``` | ||
| 103 | +Nivel Patrón Ejemplo | ||
| 104 | +────────────────────────────────── | ||
| 105 | +tipo X000 1000, 2000 | ||
| 106 | +clase XX00 1100, 1200 | ||
| 107 | +cuenta XXX0 1110, 1120 | ||
| 108 | +sub_cuenta XXXX 1111, 1112 | ||
| 109 | +``` | ||
| 110 | + | ||
| 111 | +**Sin índices.** La tabla tiene ~400 filas y se carga completa al inicio. | ||
| 112 | + | ||
| 113 | +--- | ||
| 114 | + | ||
| 115 | +### 4. `clas_finfun` - Clasificador de Finalidad y Función | ||
| 116 | + | ||
| 117 | +| Columna | Tipo | Descripción | | ||
| 118 | +|---------|------|-------------| | ||
| 119 | +| nivel | TEXT | Nivel jerárquico: 'finalidad', 'grpfuncion', 'funcion' | | ||
| 120 | +| finfun | TEXT (PK) | Código de finalidad/función (ej: "1", "11", "111") | | ||
| 121 | +| desc_finfun | TEXT | Descripción vigente de la finalidad/función | | ||
| 122 | +| gestiones | TEXT | Rangos de años donde existe (ej: "2005, 2007-2020, 2022-2026") | | ||
| 123 | +| n_gestiones | INTEGER | Cantidad de años con datos | | ||
| 124 | +| n_variaciones | INTEGER | Cantidad de variaciones históricas en la descripción | | ||
| 125 | +| descripciones | JSONB | Historial de descripciones por rangos de años | | ||
| 126 | + | ||
| 127 | +**Estructura de `descripciones`:** | ||
| 128 | +```json | ||
| 129 | +[ | ||
| 130 | + { | ||
| 131 | + "descripcion": "Descripción vigente de la finalidad/función...", | ||
| 132 | + "rangos": "2011-2020, 2022-2026" | ||
| 133 | + }, | ||
| 134 | + { | ||
| 135 | + "descripcion": "Descripción anterior...", | ||
| 136 | + "rangos": "2005, 2007-2010" | ||
| 137 | + } | ||
| 138 | +] | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +**Jerarquía derivada del código `finfun`:** | ||
| 142 | +``` | ||
| 143 | +Nivel Patrón Ejemplo | ||
| 144 | +────────────────────────────────── | ||
| 145 | +finalidad X 1, 2, 3 | ||
| 146 | +grpfuncion XX 11, 12, 21 | ||
| 147 | +funcion XXX+ 111, 112, 211 | ||
| 148 | +``` | ||
| 149 | + | ||
| 150 | +**Sin índices.** La tabla tiene ~200 filas y se carga completa al inicio. | ||
| 151 | + | ||
| 152 | +--- | ||
| 153 | + | ||
| 154 | +## Tablas de Datos Agregados | ||
| 155 | + | ||
| 156 | +### 5. `treemap_objeto` - Datos para Treemap de Objetos de Gasto | ||
| 157 | + | ||
| 158 | +| Columna | Tipo | Descripción | | ||
| 159 | +|---------|------|-------------| | ||
| 160 | +| gestion | INTEGER | Año fiscal (2005-2025) | | ||
| 161 | +| entidad | INTEGER | Código de entidad (0 = Todo el Estado) | | ||
| 162 | +| nivel | VARCHAR(20) | Nivel: 'grupo', 'subgrupo', 'partida', 'subpartida' | | ||
| 163 | +| objeto | INTEGER | Código del objeto | | ||
| 164 | +| desc_objeto | TEXT | Descripción del objeto | | ||
| 165 | +| parent | INTEGER | Código del objeto padre (para jerarquía) | | ||
| 166 | +| devengado | NUMERIC | Monto devengado en Bs | | ||
| 167 | + | ||
| 168 | +**Índices recomendados:** | ||
| 169 | +```sql | ||
| 170 | +CREATE INDEX idx_treemap_objeto_gestion_entidad ON ppto.treemap_objeto(gestion, entidad); | ||
| 171 | +``` | ||
| 172 | + | ||
| 173 | +**Notas:** | ||
| 174 | +- ~1.4 millones de filas | ||
| 175 | +- Entidad 0 representa el agregado de todo el estado | ||
| 176 | +- Cada combinación gestión + entidad debe cargarse como unidad | ||
| 177 | + | ||
| 178 | +--- | ||
| 179 | + | ||
| 180 | +### 6. `entidades_treemap` - Índice de Entidades por Año (Treemap) | ||
| 181 | + | ||
| 182 | +| Columna | Tipo | Descripción | | ||
| 183 | +|---------|------|-------------| | ||
| 184 | +| entidad | INTEGER | Código de la entidad | | ||
| 185 | +| gestion | INTEGER | Año fiscal | | ||
| 186 | +| desc_entidad | TEXT | Nombre de la entidad | | ||
| 187 | +| sigla_entidad | VARCHAR(50) | Sigla de la entidad | | ||
| 188 | + | ||
| 189 | +**Índices recomendados:** | ||
| 190 | +```sql | ||
| 191 | +CREATE INDEX idx_entidades_treemap_gestion ON ppto.entidades_treemap(gestion); | ||
| 192 | +``` | ||
| 193 | + | ||
| 194 | +**SQL para crear (derivada de treemap_objeto):** | ||
| 195 | +```sql | ||
| 196 | +CREATE TABLE ppto.entidades_treemap AS | ||
| 197 | +SELECT DISTINCT | ||
| 198 | + t.entidad, | ||
| 199 | + t.gestion, | ||
| 200 | + c.desc_entidad, | ||
| 201 | + c.sigla_entidad | ||
| 202 | +FROM ppto.treemap_objeto t | ||
| 203 | +LEFT JOIN ppto.clas_institucional c ON t.entidad = c.entidad | ||
| 204 | +WHERE t.entidad > 0; | ||
| 205 | + | ||
| 206 | +CREATE INDEX idx_entidades_treemap_gestion ON ppto.entidades_treemap(gestion); | ||
| 207 | +GRANT SELECT ON ppto.entidades_treemap TO anon, authenticated; | ||
| 208 | +``` | ||
| 209 | + | ||
| 210 | +**Uso:** Poblar dropdown de entidades filtrado por año seleccionado. | ||
| 211 | + | ||
| 212 | +--- | ||
| 213 | + | ||
| 214 | +### 7. `entidades_por_objeto` - Relación Entidades-Objetos | ||
| 215 | + | ||
| 216 | +| Columna | Tipo | Descripción | | ||
| 217 | +|---------|------|-------------| | ||
| 218 | +| entidad | INTEGER | Código de la entidad | | ||
| 219 | +| entidad_desc | TEXT | Nombre de la entidad | | ||
| 220 | +| objeto | INTEGER | Código del objeto | | ||
| 221 | +| nivel | VARCHAR(20) | Nivel del objeto | | ||
| 222 | + | ||
| 223 | +**Índices recomendados:** | ||
| 224 | +```sql | ||
| 225 | +CREATE INDEX idx_entidades_por_objeto_objeto ON ppto.entidades_por_objeto(objeto); | ||
| 226 | +CREATE INDEX idx_entidades_por_objeto_objeto_nivel ON ppto.entidades_por_objeto(objeto, nivel); | ||
| 227 | +``` | ||
| 228 | + | ||
| 229 | +**Uso:** Poblar dropdowns de entidades al ver un objeto específico. | ||
| 230 | + | ||
| 231 | +--- | ||
| 232 | + | ||
| 233 | +### 8. `vista_finfun_estado` - Vista Agregada por Finalidad/Función (Todo el Estado) | ||
| 234 | + | ||
| 235 | +| Columna | Tipo | Descripción | | ||
| 236 | +|---------|------|-------------| | ||
| 237 | +| gestion | INTEGER | Año fiscal | | ||
| 238 | +| finfun | TEXT | Código de finalidad/función | | ||
| 239 | +| total | NUMERIC | Monto total devengado | | ||
| 240 | +| poblacion | INTEGER | Población del año | | ||
| 241 | +| n_entidades | INTEGER | Cantidad de entidades con datos | | ||
| 242 | +| per_capita | NUMERIC | Gasto per cápita (total/población) | | ||
| 243 | +| total_gestion | NUMERIC | Total del Estado en esa gestión | | ||
| 244 | +| prop | NUMERIC | Proporción sobre el total del Estado | | ||
| 245 | +| ranking | INTEGER | Ranking por monto dentro de su nivel | | ||
| 246 | +| n_pares | INTEGER | Cantidad de códigos del mismo nivel | | ||
| 247 | +| top1_entidad | INTEGER | Código de la entidad con mayor gasto | | ||
| 248 | +| top1_entidad_desc | TEXT | Nombre de la entidad top 1 | | ||
| 249 | +| top1_monto | NUMERIC | Monto de la entidad top 1 | | ||
| 250 | +| top1_pct | NUMERIC | Porcentaje del total | | ||
| 251 | +| top2_entidad | INTEGER | Código de la entidad top 2 | | ||
| 252 | +| top2_entidad_desc | TEXT | Nombre de la entidad top 2 | | ||
| 253 | +| top2_monto | NUMERIC | Monto de la entidad top 2 | | ||
| 254 | +| top2_pct | NUMERIC | Porcentaje del total | | ||
| 255 | +| top3_entidad | INTEGER | Código de la entidad top 3 | | ||
| 256 | +| top3_entidad_desc | TEXT | Nombre de la entidad top 3 | | ||
| 257 | +| top3_monto | NUMERIC | Monto de la entidad top 3 | | ||
| 258 | +| top3_pct | NUMERIC | Porcentaje del total | | ||
| 259 | + | ||
| 260 | +**Índices recomendados:** | ||
| 261 | +```sql | ||
| 262 | +CREATE INDEX idx_vista_finfun_estado_finfun ON ppto.vista_finfun_estado(finfun); | ||
| 263 | +CREATE INDEX idx_vista_finfun_estado_finfun_gestion ON ppto.vista_finfun_estado(finfun, gestion); | ||
| 264 | +``` | ||
| 265 | + | ||
| 266 | +--- | ||
| 267 | + | ||
| 268 | +### 9. `vista_finfun_entidad` - Vista por Finalidad/Función y Entidad | ||
| 269 | + | ||
| 270 | +| Columna | Tipo | Descripción | | ||
| 271 | +|---------|------|-------------| | ||
| 272 | +| gestion | INTEGER | Año fiscal | | ||
| 273 | +| finfun | TEXT | Código de finalidad/función | | ||
| 274 | +| nivel | TEXT | Nivel: 'finalidad', 'grpfuncion', 'funcion' | | ||
| 275 | +| entidad | INTEGER | Código de la entidad | | ||
| 276 | +| entidad_desc | TEXT | Nombre de la entidad | | ||
| 277 | +| monto | NUMERIC | Monto devengado | | ||
| 278 | +| per_capita | NUMERIC | Gasto per cápita | | ||
| 279 | +| ranking | INTEGER | Ranking de la entidad para ese finfun | | ||
| 280 | +| prop | NUMERIC | Proporción sobre el total del finfun | | ||
| 281 | +| n_entidades | INTEGER | Total de entidades con ese finfun | | ||
| 282 | + | ||
| 283 | +**Índices recomendados:** | ||
| 284 | +```sql | ||
| 285 | +CREATE INDEX idx_vista_finfun_entidad_finfun ON ppto.vista_finfun_entidad(finfun); | ||
| 286 | +CREATE INDEX idx_vista_finfun_entidad_finfun_entidad ON ppto.vista_finfun_entidad(finfun, entidad); | ||
| 287 | +``` | ||
| 288 | + | ||
| 289 | +--- | ||
| 290 | + | ||
| 291 | +### 10. `entidades_por_finfun` - Relación Entidades-Finalidad/Función | ||
| 292 | + | ||
| 293 | +| Columna | Tipo | Descripción | | ||
| 294 | +|---------|------|-------------| | ||
| 295 | +| finfun | TEXT | Código de finalidad/función | | ||
| 296 | +| nivel | TEXT | Nivel: 'finalidad', 'grpfuncion', 'funcion' | | ||
| 297 | +| entidad | INTEGER | Código de la entidad | | ||
| 298 | +| entidad_desc | TEXT | Nombre de la entidad | | ||
| 299 | + | ||
| 300 | +**Índices recomendados:** | ||
| 301 | +```sql | ||
| 302 | +CREATE INDEX idx_entidades_por_finfun_finfun ON ppto.entidades_por_finfun(finfun); | ||
| 303 | +CREATE INDEX idx_entidades_por_finfun_finfun_nivel ON ppto.entidades_por_finfun(finfun, nivel); | ||
| 304 | +``` | ||
| 305 | + | ||
| 306 | +**Uso:** Poblar dropdowns de entidades al ver una finalidad/función específica. | ||
| 307 | + | ||
| 308 | +--- | ||
| 309 | + | ||
| 310 | +## Vistas Materializadas / Tablas de Consulta | ||
| 311 | + | ||
| 312 | +### 11. `vista_objeto_estado` - Vista Agregada por Objeto (Todo el Estado) | ||
| 313 | + | ||
| 314 | +| Columna | Tipo | Descripción | | ||
| 315 | +|---------|------|-------------| | ||
| 316 | +| objeto | INTEGER | Código del objeto | | ||
| 317 | +| gestion | INTEGER | Año fiscal | | ||
| 318 | +| devengado | NUMERIC | Monto total devengado | | ||
| 319 | +| top1_entidad | TEXT | Entidad con mayor gasto | | ||
| 320 | +| top1_monto | NUMERIC | Monto de la entidad top 1 | | ||
| 321 | +| top1_pct | NUMERIC | Porcentaje del total | | ||
| 322 | +| top2_entidad | TEXT | Segunda entidad con mayor gasto | | ||
| 323 | +| top2_monto | NUMERIC | Monto de la entidad top 2 | | ||
| 324 | +| top2_pct | NUMERIC | Porcentaje del total | | ||
| 325 | +| top3_entidad | TEXT | Tercera entidad con mayor gasto | | ||
| 326 | +| top3_monto | NUMERIC | Monto de la entidad top 3 | | ||
| 327 | +| top3_pct | NUMERIC | Porcentaje del total | | ||
| 328 | + | ||
| 329 | +**Índices recomendados:** | ||
| 330 | +```sql | ||
| 331 | +CREATE INDEX idx_vista_objeto_estado_objeto ON ppto.vista_objeto_estado(objeto); | ||
| 332 | +CREATE INDEX idx_vista_objeto_estado_objeto_gestion ON ppto.vista_objeto_estado(objeto, gestion); | ||
| 333 | +``` | ||
| 334 | + | ||
| 335 | +--- | ||
| 336 | + | ||
| 337 | +### 12. `vista_objeto_entidad` - Vista por Objeto y Entidad | ||
| 338 | + | ||
| 339 | +| Columna | Tipo | Descripción | | ||
| 340 | +|---------|------|-------------| | ||
| 341 | +| objeto | INTEGER | Código del objeto | | ||
| 342 | +| nivel | VARCHAR(20) | Nivel del objeto | | ||
| 343 | +| entidad | INTEGER | Código de la entidad | | ||
| 344 | +| gestion | INTEGER | Año fiscal | | ||
| 345 | +| devengado | NUMERIC | Monto devengado | | ||
| 346 | + | ||
| 347 | +**Índices recomendados:** | ||
| 348 | +```sql | ||
| 349 | +CREATE INDEX idx_vista_objeto_entidad_objeto ON ppto.vista_objeto_entidad(objeto); | ||
| 350 | +CREATE INDEX idx_vista_objeto_entidad_objeto_entidad ON ppto.vista_objeto_entidad(objeto, entidad); | ||
| 351 | +CREATE INDEX idx_vista_objeto_entidad_objeto_nivel_entidad ON ppto.vista_objeto_entidad(objeto, nivel, entidad); | ||
| 352 | +``` | ||
| 353 | + | ||
| 354 | +--- | ||
| 355 | + | ||
| 356 | +## Jerarquías | ||
| 357 | + | ||
| 358 | +### Objetos de Gasto (4 niveles) | ||
| 359 | +``` | ||
| 360 | +Grupo (1xxxx) → Subgrupo (11xxx) → Partida (111xx) → Subpartida (11110) | ||
| 361 | +``` | ||
| 362 | + | ||
| 363 | +### Rubros de Ingresos (4 niveles) | ||
| 364 | +``` | ||
| 365 | +Tipo → Clase → Cuenta → Subcuenta | ||
| 366 | +``` | ||
| 367 | + | ||
| 368 | +### Finalidad y Función (3 niveles) | ||
| 369 | +``` | ||
| 370 | +Finalidad (X) → Grupo Función (XX) → Función (XXX) | ||
| 371 | +``` | ||
| 372 | + | ||
| 373 | +### Institucional (5 niveles) | ||
| 374 | +``` | ||
| 375 | +Sector → Subsector → Área → Subárea → Entidad | ||
| 376 | +``` | ||
| 377 | + | ||
| 378 | +--- | ||
| 379 | + | ||
| 380 | + | ||
| 381 | + | ||
| 382 | +*Documento generado para el equipo de sistemas - Marzo 2026* |
-
Please register or login to post a comment