Toggle navigation
Toggle navigation
This project
Loading...
Sign in
Rafael Lopez
/
ppto
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
Rafael Lopez
2026-03-15 16:24:10 -0400
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
1d184ff18c39733e7c7103a62e692e3295a139b0
1d184ff1
1 parent
d6ade18e
obj done
Show whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
743 additions
and
127 deletions
src/lib/components/objeto/BarChart.svelte
src/lib/components/objeto/ObjetoSearch.svelte
src/lib/components/objeto/VistaToggle.svelte
src/routes/objeto/[codigo]/+page.svelte
static/fail.png
static/problem.png
src/lib/components/objeto/BarChart.svelte
View file @
1d184ff
...
...
@@ -178,6 +178,10 @@
min-height: 2px;
}
:global(html.dark) .bar {
background: rgba(107, 159, 212, 0.85);
}
.bars:hover .bar {
opacity: 0.35;
}
...
...
src/lib/components/objeto/ObjetoSearch.svelte
View file @
1d184ff
...
...
@@ -11,6 +11,11 @@
navigateResults
} from '$lib/stores/searchStore';
let {
preserveVista = false,
vista = 'agregado'
} = $props();
let searchInputRef = $state(null);
let searchVal = $state('');
let searchFocused = $state(false);
...
...
@@ -47,7 +52,10 @@
}
function handleSearchSelect(result) {
goto(`/objeto/${result.codigo}`);
const url = preserveVista && vista === 'comparar'
? `/objeto/${result.codigo}?vista=comparar`
: `/objeto/${result.codigo}`;
goto(url);
searchVal = '';
clearSearch();
searchFocused = false;
...
...
@@ -116,7 +124,7 @@
display: flex;
align-items: center;
gap: 0.5rem;
background:
#f5f5f5
;
background:
var(--search-bg, #f5f5f5)
;
border: none;
border-radius: 8px;
padding: 0.625rem 1rem;
...
...
@@ -124,6 +132,10 @@
width: 100%;
}
:global(html.dark) .objeto-search {
--search-bg: rgba(255, 255, 255, 0.08);
}
.objeto-search.focused {
border-color: #6B9FD4;
box-shadow: 0 0 0 3px rgba(107, 159, 212, 0.15);
...
...
src/lib/components/objeto/VistaToggle.svelte
View file @
1d184ff
...
...
@@ -57,4 +57,14 @@
.vista-btn:active {
color: #555555;
}
:global(html.dark) .vista-btn {
color: rgba(255, 255, 255, 0.6);
text-decoration-color: rgba(255, 255, 255, 0.4);
}
:global(html.dark) .vista-btn:hover {
color: rgba(255, 255, 255, 0.85);
text-decoration-color: rgba(255, 255, 255, 0.5);
}
</style>
...
...
src/routes/objeto/[codigo]/+page.svelte
View file @
1d184ff
...
...
@@ -3,6 +3,7 @@
import { tweened } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { supabase } from '$lib/supabase';
import Spinner from '$lib/components/ui/Spinner.svelte';
import ObjetoSearch from '$lib/components/objeto/ObjetoSearch.svelte';
...
...
@@ -30,9 +31,88 @@
let showInfo = $state(false);
let showDefinicionesModal = $state(false);
let showMobileSidebar = $state(false);
// Estado de vista
let vista = $state('agregado'); // 'agregado' | 'comparar'
let isDark = $state(true);
// ══════════════════════════════════════════════════════════════
// MANEJO DE URL - Estado compartible
// ══════════════════════════════════════════════════════════════
// Leer parámetros de URL
function getUrlParams() {
const params = $page.url.searchParams;
return {
vista: params.get('vista') || 'agregado',
entidad: params.get('entidad') ? parseInt(params.get('entidad')) : null,
entidadA: params.get('entidadA') ? parseInt(params.get('entidadA')) : null,
entidadB: params.get('entidadB') ? parseInt(params.get('entidadB')) : null
};
}
// Actualizar URL sin recargar la página
function updateUrl(newParams = {}) {
const url = new URL($page.url);
// Limpiar parámetros anteriores
url.searchParams.delete('vista');
url.searchParams.delete('entidad');
url.searchParams.delete('entidadA');
url.searchParams.delete('entidadB');
// Agregar nuevos parámetros según el modo
if (newParams.vista === 'comparar') {
url.searchParams.set('vista', 'comparar');
if (newParams.entidadA) url.searchParams.set('entidadA', newParams.entidadA);
if (newParams.entidadB) url.searchParams.set('entidadB', newParams.entidadB);
} else {
// Vista agregada - solo agregar entidad si hay una seleccionada
if (newParams.entidad) url.searchParams.set('entidad', newParams.entidad);
}
goto(url.toString(), { replaceState: true, noScroll: true });
}
// Aplicar estado desde URL después de cargar entidades
async function applyUrlState() {
const params = getUrlParams();
// Aplicar vista
vista = params.vista;
if (params.vista === 'comparar') {
// Modo comparación
if (params.entidadA) {
const entityA = entidades.find(e => e.entidad === params.entidadA);
if (entityA) {
entidadCompararA = entityA;
datosCompararA = await loadDatosEntidad(entityA);
}
}
if (params.entidadB) {
const entityB = entidades.find(e => e.entidad === params.entidadB);
if (entityB) {
entidadCompararB = entityB;
datosCompararB = await loadDatosEntidad(entityB);
}
}
// Si no hay entidades en URL, inicializar con defaults
if (!params.entidadA && !params.entidadB) {
await initComparacion();
}
} else {
// Vista agregada
if (params.entidad) {
const entity = entidades.find(e => e.entidad === params.entidad);
if (entity) {
selectedEntity = entity;
await loadData();
}
}
}
}
// Parsear descripciones del objeto
function parseDescripciones(descripcionesStr) {
...
...
@@ -407,11 +487,53 @@
let hoveredYearComparar = $state(null);
let loadingComparacion = $state(false);
let isDefaultSelection = $state(false); // true si la selección es automática
// Estado de alertas (cerradas por el usuario)
let alertDefaultDismissed = $state(false);
let alertRangosDismissed = $state(false);
// Entidades seleccionadas para comparar
let entidadCompararA = $state(null);
let entidadCompararB = $state(null);
// Pre-seleccionar entidades al cambiar a modo comparar
async function initComparacion() {
// Si ya hay selección manual, no hacer nada
if (entidadCompararA || entidadCompararB) return;
if (selectedEntity) {
// Si venimos de una entidad específica, pre-seleccionarla en A
entidadCompararA = selectedEntity;
loadingComparacion = true;
datosCompararA = await loadDatosEntidad(selectedEntity);
loadingComparacion = false;
isDefaultSelection = false; // No es default porque el usuario ya eligió una entidad
} else if (topEntidadesTotal.length >= 2) {
// Si venimos de agregado, pre-seleccionar top 2 del ranking
const top1 = entidades.find(e => e.entidad_desc === topEntidadesTotal[0].nombre);
const top2 = entidades.find(e => e.entidad_desc === topEntidadesTotal[1].nombre);
if (top1 && top2) {
isDefaultSelection = true;
loadingComparacion = true;
entidadCompararA = top1;
datosCompararA = await loadDatosEntidad(top1);
entidadCompararB = top2;
datosCompararB = await loadDatosEntidad(top2);
loadingComparacion = false;
}
}
}
// Limpiar selección por defecto cuando el usuario cambia manualmente
function clearDefaultSelection() {
isDefaultSelection = false;
}
// Datos cargados de cada entidad
let datosCompararA = $state({ anual: [], historico: null });
let datosCompararB = $state({ anual: [], historico: null });
...
...
@@ -467,6 +589,9 @@
dropdownAOpen = false;
searchA = '';
entityLimitA = 30;
isDefaultSelection = false; // Selección manual
alertDefaultDismissed = false; // Resetear alerta
alertRangosDismissed = false; // Mostrar alerta de rangos si aplica
loadingComparacion = true;
datosCompararA = await loadDatosEntidad(entidad);
loadingComparacion = false;
...
...
@@ -478,20 +603,58 @@
dropdownBOpen = false;
searchB = '';
entityLimitB = 30;
isDefaultSelection = false; // Selección manual
alertDefaultDismissed = false; // Resetear alerta
alertRangosDismissed = false; // Mostrar alerta de rangos si aplica
loadingComparacion = true;
datosCompararB = await loadDatosEntidad(entidad);
loadingComparacion = false;
}
// Selector de rango de años
let rangoAños = $state('
10
'); // '5', '10', '15', 'todo'
let rangoAños = $state('
todo
'); // '5', '10', '15', 'todo'
// Años
comunes entre ambas entidades
// Años
para comparación (comunes si hay 2 entidades, o los de la única seleccionada)
let añosComunesTotales = $derived.by(() => {
if (!datosCompararA.anual.length || !datosCompararB.anual.length) return [];
const añosA = new Set(datosCompararA.anual.map(d => d.gestion));
const añosB = new Set(datosCompararB.anual.map(d => d.gestion));
return [...añosA].filter(a => añosB.has(a)).sort((a, b) => a - b);
const añosA = datosCompararA.anual.map(d => d.gestion);
const añosB = datosCompararB.anual.map(d => d.gestion);
// Si hay ambas entidades, usar años en común
if (añosA.length && añosB.length) {
const setB = new Set(añosB);
return añosA.filter(a => setB.has(a)).sort((a, b) => a - b);
}
// Si solo hay A, usar sus años
if (añosA.length) return [...añosA].sort((a, b) => a - b);
// Si solo hay B, usar sus años
if (añosB.length) return [...añosB].sort((a, b) => a - b);
return [];
});
// Flag para saber si estamos en modo "solo una entidad"
let soloUnaEntidad = $derived(
(entidadCompararA && !entidadCompararB) || (!entidadCompararA && entidadCompararB)
);
// Información de rangos para advertencia
let rangoInfoA = $derived.by(() => {
if (!datosCompararA.anual.length) return null;
const años = datosCompararA.anual.map(d => d.gestion).sort((a, b) => a - b);
return { desde: años[0], hasta: años[años.length - 1], total: años.length };
});
let rangoInfoB = $derived.by(() => {
if (!datosCompararB.anual.length) return null;
const años = datosCompararB.anual.map(d => d.gestion).sort((a, b) => a - b);
return { desde: años[0], hasta: años[años.length - 1], total: años.length };
});
// Detectar si hay diferencia significativa en los rangos
let tienenRangosDiferentes = $derived.by(() => {
if (!rangoInfoA || !rangoInfoB) return false;
return rangoInfoA.desde !== rangoInfoB.desde ||
rangoInfoA.hasta !== rangoInfoB.hasta ||
rangoInfoA.total !== rangoInfoB.total;
});
// Años filtrados según el rango seleccionado
...
...
@@ -580,8 +743,25 @@
});
// Colores
let colorA = '#2a9d8f';
let colorB = '#f4a261';
// Colores para comparación (reactivos al tema)
let isDarkMode = $state(false);
$effect(() => {
if (typeof document !== 'undefined') {
const checkDark = () => {
isDarkMode = document.documentElement.classList.contains('dark');
};
checkDark();
const observer = new MutationObserver(checkDark);
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
}
});
// Modo claro: gris-azulado pastel + terracota
// Modo oscuro: azul claro + ámbar dorado
let colorA = $derived(isDarkMode ? '#7EB8DA' : '#6B8299');
let colorB = $derived(isDarkMode ? '#D4A574' : '#C4897D');
// Actualizar tweened de comparación
$effect(() => {
...
...
@@ -673,10 +853,13 @@
attributeFilter: ['class']
});
// Cargar datos
// Cargar datos
base y entidades
const loadStart = performance.now();
await Promise.all([loadData(), loadEntidades()]);
console.log(`[CLIENT] Promise.all: ${(performance.now() - loadStart).toFixed(0)}ms`);
// Aplicar estado desde URL (después de tener entidades cargadas)
await applyUrlState();
console.log(`[CLIENT] onMount total: ${(performance.now() - mountStart).toFixed(0)}ms`);
// Cerrar dropdowns al hacer click fuera
...
...
@@ -705,16 +888,21 @@
$effect(() => {
if (mounted && objetoCodigo !== previousCodigo) {
previousCodigo = objetoCodigo;
// Resetear estado
// Resetear estado
base
loading = true;
selectedEntity = null;
entidadCompararA = null;
entidadCompararB = null;
datosCompararA = { anual: [], historico: null };
datosCompararB = { anual: [], historico: null };
vista = 'agregado';
// Recargar datos
loadData();
alertDefaultDismissed = false;
alertRangosDismissed = false;
// Recargar datos y entidades, luego aplicar URL
(async () => {
await Promise.all([loadData(), loadEntidades()]);
await applyUrlState();
})();
}
});
...
...
@@ -724,6 +912,49 @@
if (mounted && selectedEntity !== previousEntity) {
previousEntity = selectedEntity;
loadData();
// Actualizar URL
updateUrl({
vista: vista,
entidad: selectedEntity?.entidad || null
});
}
});
// Actualizar URL cuando cambian entidades de comparación
let prevEntidadA = entidadCompararA;
let prevEntidadB = entidadCompararB;
$effect(() => {
if (mounted && vista === 'comparar') {
if (prevEntidadA !== entidadCompararA || prevEntidadB !== entidadCompararB) {
prevEntidadA = entidadCompararA;
prevEntidadB = entidadCompararB;
updateUrl({
vista: 'comparar',
entidadA: entidadCompararA?.entidad || null,
entidadB: entidadCompararB?.entidad || null
});
}
}
});
// Actualizar URL cuando cambia el modo vista
let previousVista = vista;
$effect(() => {
if (mounted && vista !== previousVista) {
previousVista = vista;
if (vista === 'comparar') {
initComparacion();
updateUrl({
vista: 'comparar',
entidadA: entidadCompararA?.entidad || null,
entidadB: entidadCompararB?.entidad || null
});
} else {
updateUrl({
vista: 'agregado',
entidad: selectedEntity?.entidad || null
});
}
}
});
...
...
@@ -991,13 +1222,21 @@
{/if}
{:else}
<!-- VISTA COMPARAR (mantiene layout original) -->
<div class="toolbar">
<ObjetoSearch />
<!-- VISTA COMPARAR -->
<main class="main-comparar">
<!-- Card principal del gráfico -->
<div class="chart-card-comparar">
<!-- Header: pills + toggle -->
<div class="chart-top-row">
<div class="main-title-pills">
<span class="pill pill-nivel">{objeto.nivel}</span>
<span class="pill pill-codigo">{objeto.codigo}</span>
</div>
<VistaToggle bind:vista />
</div>
<main>
<!-- VISTA COMPARAR -->
<!-- Título principal -->
<h2 class="main-title-name">{objeto.nombre}</h2>
<!-- Selectores de entidades con búsqueda -->
<div class="comparador-selectors">
...
...
@@ -1080,20 +1319,65 @@
{/if}
</div>
</div>
<!-- Buscador de objeto de gasto -->
<div class="comparador-search-wrapper">
<ObjetoSearch preserveVista={true} {vista} />
</div>
</div>
<!-- Mensaje si no hay entidades seleccionadas -->
{#if !entidadCompararA || !entidadCompararB}
<div class="comparador-placeholder">
<p>Selecciona dos entidades para comparar su gasto en <strong>{objeto.nombre}</strong></p>
<!-- Mensaje de selección por defecto -->
{#if isDefaultSelection && entidadCompararA && entidadCompararB && !alertDefaultDismissed}
<div class="comparador-hint">
<svg class="alert-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<line x1="12" y1="16" x2="12" y2="12"/>
<line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<span>Mostrando las dos entidades con mayor gasto. Puedes cambiar la selección arriba.</span>
<button class="alert-close" onclick={() => alertDefaultDismissed = true} aria-label="Cerrar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
{:else if loadingComparacion}
{/if}
<!-- Advertencia de rangos diferentes -->
{#if tienenRangosDiferentes && entidadCompararA && entidadCompararB && !loadingComparacion && !alertRangosDismissed}
<div class="comparador-warning">
<svg class="alert-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<span>
Rangos de tiempo diferentes:
<strong>{entidadCompararA.entidad_desc.substring(0, 20)}{entidadCompararA.entidad_desc.length > 20 ? '...' : ''}</strong> ({rangoInfoA?.desde}-{rangoInfoA?.hasta})
vs
<strong>{entidadCompararB.entidad_desc.substring(0, 20)}{entidadCompararB.entidad_desc.length > 20 ? '...' : ''}</strong> ({rangoInfoB?.desde}-{rangoInfoB?.hasta}).
Solo se comparan {añosComunesTotales.length} años en común.
</span>
<button class="alert-close" onclick={() => alertRangosDismissed = true} aria-label="Cerrar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
{/if}
<!-- Estados del comparador -->
{#if loadingComparacion}
<div class="comparador-placeholder">
<Spinner size={48} color="var(--theme-texto, #5A5650)" />
</div>
{:else if !entidadCompararA && !entidadCompararB}
<div class="comparador-placeholder">
<p>Selecciona dos entidades para comparar su gasto en <strong>{objeto.nombre}</strong></p>
</div>
{:else if añosComparacion.length === 0}
<div class="comparador-placeholder">
<p>
Las entidades seleccionadas no tienen años en común para este objeto
.</p>
<p>
No hay datos disponibles para comparar
.</p>
</div>
{:else}
<!-- Selector de rango de años -->
...
...
@@ -1110,28 +1394,28 @@
{/if}
<!-- Gráfico comparativo -->
<div class="chart-section">
<div class="chart-header">
<span class="chart-title">Bs/habitante</span>
<span class="chart-period">{displayPeriodoComparar}</span>
<span class="chart-title">Bs/habitante <span class="chart-period">· {displayPeriodoComparar}</span></span>
</div>
<div class="chart-wrapper chart-wrapper-scroll">
<div class="y-axis y-axis-sticky">
<span class="y-label">Bs {formatMonto(Math.round(maxComparacion))}</span>
<span class="y-label">Bs {formatMonto(Math.round(maxComparacion / 2))}</span>
<span class="y-label">Bs 0</span>
</div>
<div class="chart-comparar-wrapper">
<!-- Área principal del gráfico -->
<div class="chart-comparar-inner">
<!-- Y-axis labels -->
<span class="y-label-comparar" style="bottom: 100%">Bs {formatMonto(Math.round(maxComparacion))}</span>
<span class="y-label-comparar" style="bottom: 50%">Bs {formatMonto(Math.round(maxComparacion / 2))}</span>
<span class="y-label-comparar" style="bottom: 0%">Bs 0</span>
<div
class="chart chart-comparar"
class:chart-muchos-años={añosComparacion.length > 15}
onmouseleave={() => hoveredYearComparar = null}
role="group"
>
<!-- Grid lines -->
<div class="grid-line-comparar" style="bottom: 100%"></div>
<div class="grid-line-comparar" style="bottom: 50%"></div>
<div class="grid-line-comparar" style="bottom: 0%"></div>
<!-- Barras -->
<div class="bars-comparar" onmouseleave={() => hoveredYearComparar = null} role="group">
{#each añosComparacion as año, i}
<div
class="bar-group"
class="bar-group
-comparar
"
class:active={hoveredYearComparar === año}
onmouseenter={() => hoveredYearComparar = año}
role="button"
...
...
@@ -1139,33 +1423,62 @@
>
<div class="bars-pair">
<div
class="bar
bar-a
"
style="height: {((datosGraficoA[i]?.per_capita || 0) / maxComparacion) * 100}%;
--bar-color
: {colorA}"
class="bar
-comparar
"
style="height: {((datosGraficoA[i]?.per_capita || 0) / maxComparacion) * 100}%;
background
: {colorA}"
></div>
<div
class="bar
bar-b
"
style="height: {((datosGraficoB[i]?.per_capita || 0) / maxComparacion) * 100}%;
--bar-color
: {colorB}"
class="bar
-comparar
"
style="height: {((datosGraficoB[i]?.per_capita || 0) / maxComparacion) * 100}%;
background
: {colorB}"
></div>
</div>
<span class="bar-label visible" class:highlight={hoveredYearComparar === año}>{año}</span>
</div>
{/each}
</div>
</div>
<!-- Eje X -->
<div class="x-axis-comparar">
{#each añosComparacion as año, i}
{@const isFirst = i === 0}
{@const isLast = i === añosComparacion.length - 1}
{@const isMid = i === Math.floor(añosComparacion.length / 2) && añosComparacion.length > 5}
{@const showLabel = isFirst || isLast || isMid || hoveredYearComparar === año}
<span class="x-label-comparar" class:visible={showLabel}>{año}</span>
{/each}
</div>
</div>
<div class="chart-legend">
{#if entidadCompararA}
<span class="legend-item"><span class="legend-dot" style="background: {colorA}"></span>{entidadCompararA.entidad_desc}</span>
{/if}
{#if entidadCompararB}
<span class="legend-item"><span class="legend-dot" style="background: {colorB}"></span>{entidadCompararB.entidad_desc}</span>
{:else}
<span class="legend-item legend-pending"><span class="legend-dot" style="background: {colorB}; opacity: 0.3"></span>Selecciona una entidad</span>
{/if}
</div>
{/if}
</div>
<!-- KPIs comparativos - tabla -->
<div class="kpis-tabla">
<!-- Mensaje para seleccionar segunda entidad -->
{#if soloUnaEntidad}
<div class="comparador-hint comparador-hint-select">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
<span>Selecciona la segunda entidad arriba para ver la comparación completa.</span>
</div>
{/if}
<!-- KPIs comparativos - tabla (solo cuando hay ambas entidades) -->
{#if entidadCompararA && entidadCompararB}
<div class="kpis-card-comparar">
<!-- Header -->
<div class="tabla-header">
<span class="tabla-label"></span>
<span class="tabla-val
" style="color: {colorA}">{entidadCompararA.entidad_desc?.slice(0, 20)}{entidadCompararA.entidad_desc?.length > 20 ? '...' : ''
}</span>
<span class="tabla-val
" style="color: {colorB}">{entidadCompararB.entidad_desc?.slice(0, 20)}{entidadCompararB.entidad_desc?.length > 20 ? '...' : ''
}</span>
<span class="tabla-val
tabla-entidad" style="color: {colorA}">{entidadCompararA.entidad_desc
}</span>
<span class="tabla-val
tabla-entidad" style="color: {colorB}">{entidadCompararB.entidad_desc
}</span>
</div>
<!-- Gasto -->
<div class="tabla-row">
...
...
@@ -1823,6 +2136,31 @@
border-left-color: var(--theme-accent);
}
:global(html.dark) .tree-node-current .tree-code,
:global(html.dark) .tree-node-current .tree-name {
color: #1a1a1a;
}
/* Dark mode - Context card texts */
:global(html.dark) .context-desc,
:global(html.dark) .context-hint {
color: rgba(255, 255, 255, 0.6);
}
/* Dark mode - Definición */
:global(html.dark) .def-status,
:global(html.dark) .def-btn {
color: rgba(255, 255, 255, 0.7);
}
:global(html.dark) .def-btn {
background: rgba(255, 255, 255, 0.1);
}
:global(html.dark) .def-btn:hover {
background: rgba(255, 255, 255, 0.15);
}
:global(html.dark) .tree-children {
border-left-color: rgba(201, 167, 81, 0.3);
}
...
...
@@ -2086,8 +2424,9 @@
background: var(--theme-fill);
border: 1px solid var(--theme-borde);
border-radius: 6px;
font-family: 'Qanelas', var(--font-sans);
color: var(--theme-titulo);
font-size: 0.8
7
5rem;
font-size: 0.8
12
5rem;
}
.search-input:focus {
outline: none;
...
...
@@ -2105,6 +2444,7 @@
padding: 0.625rem 0.75rem;
background: transparent;
border: none;
font-family: 'Qanelas', var(--font-sans);
color: var(--theme-titulo);
font-size: 0.813rem;
text-align: left;
...
...
@@ -2176,21 +2516,12 @@
padding-bottom: 1.5rem;
}
.chart-section:has(.chart-comparar) {
margin-bottom: 0;
}
.chart-section:has(.chart-comparar) .y-axis {
height: calc(100vh - 380px);
min-height: 140px;
max-height: 280px;
}
.chart-top-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.375rem;
flex-shrink: 0;
}
.main-title-pills {
...
...
@@ -2223,10 +2554,12 @@
color: var(--theme-titulo);
margin: 0 0 0.5rem 0;
line-height: 1.2;
flex-shrink: 0;
}
.chart-header {
margin-bottom: 0.5rem;
flex-shrink: 0;
}
.chart-title {
...
...
@@ -2260,6 +2593,7 @@
.chart-wrapper {
display: flex;
gap: 0.75rem;
position: relative;
}
.chart-wrapper-scroll {
...
...
@@ -2295,10 +2629,6 @@
min-width: max-content;
}
.chart-muchos-años .bar-group {
min-width: 40px;
}
.y-axis {
display: flex;
flex-direction: column;
...
...
@@ -2390,9 +2720,18 @@
text-align: center;
min-width: 0;
padding: 0.625rem 0.5rem;
background:
var(--theme-body)
;
background:
transparent
;
border-radius: 8px;
border: 1px solid var(--theme-borde);
border: 1px solid rgba(0, 0, 0, 0.08);
}
:global(html.dark) .kpi {
border-color: rgba(255, 255, 255, 0.1);
}
:global(html.dark) .kpi-value,
:global(html.dark) .kpi-label {
color: rgba(255, 255, 255, 0.7);
}
.kpi-value {
...
...
@@ -2507,6 +2846,20 @@
text-align: right;
}
/* Dark mode - Ranking texts */
:global(html.dark) .ranking-desc {
color: rgba(255, 255, 255, 0.6);
}
:global(html.dark) .ranking-position,
:global(html.dark) .ranking-pct {
color: rgba(255, 255, 255, 0.85);
}
:global(html.dark) .ranking-name {
color: rgba(255, 255, 255, 0.7);
}
.see-all {
background: transparent;
border: 1px solid var(--theme-borde);
...
...
@@ -2525,25 +2878,82 @@
}
/* Comparador inline */
.main-comparar {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.75rem 2rem 1.5rem;
width: 100%;
max-width: 1200px;
margin: 0 auto;
min-height: calc(100vh - 60px);
}
.chart-card-comparar {
background: var(--theme-surface);
border-radius: 16px;
padding: 1rem 1.5rem;
margin-bottom: 0.75rem;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.comparador-selectors {
display: flex;
align-items:
flex-start
;
align-items:
center
;
gap: 1rem;
margin-bottom: 0.75rem;
margin-bottom: 1rem;
flex-shrink: 0;
}
.comparador-search-wrapper {
flex: 1;
max-width: 280px;
}
.comparador-dropdown-container {
position: relative;
}
.selector-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1rem;
background: var(--theme-body);
border: 1px solid var(--theme-borde);
border-radius: 8px;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.8125rem;
color: var(--theme-titulo);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.selector-btn:hover {
border-color: var(--theme-accent);
background: var(--theme-surface-hover);
}
.comparador-btn {
width: 100%;
justify-content: space-between;
}
.comparador-panel {
position: absolute;
top: calc(100% + 4px);
left: 0;
width: 100%;
min-width: 280px;
background: var(--theme-surface);
border: 1px solid var(--theme-borde);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
z-index: 100;
overflow: hidden;
}
.comparador-placeholder {
...
...
@@ -2559,6 +2969,7 @@
}
.comparador-placeholder p {
font-family: 'Qanelas', var(--font-sans);
color: var(--theme-texto);
font-size: 0.875rem;
margin: 0;
...
...
@@ -2568,14 +2979,99 @@
color: var(--theme-texto);
}
.comparador-hint {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: rgba(107, 159, 212, 0.1);
border-radius: 8px;
margin-bottom: 0.75rem;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.8125rem;
color: #6B9FD4;
flex-shrink: 0;
}
.comparador-hint .alert-icon {
flex-shrink: 0;
margin-top: 0.1rem;
}
.comparador-hint span {
flex: 1;
}
.comparador-warning {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem 1rem;
background: rgba(230, 176, 75, 0.1);
border-radius: 8px;
margin-bottom: 0.75rem;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.8125rem;
color: #C9A751;
flex-shrink: 0;
}
.comparador-warning .alert-icon {
flex-shrink: 0;
margin-top: 0.1rem;
}
.comparador-warning span {
flex: 1;
}
.comparador-warning strong {
color: inherit;
}
/* Botón cerrar alertas */
.alert-close {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border: none;
background: transparent;
color: inherit;
opacity: 0.6;
cursor: pointer;
border-radius: 4px;
transition: opacity 0.15s, background 0.15s;
}
.alert-close:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.1);
}
:global(html.dark) .alert-close:hover {
background: rgba(255, 255, 255, 0.1);
}
:global(html.dark) .comparador-hint {
background: rgba(107, 159, 212, 0.15);
}
:global(html.dark) .comparador-warning {
background: rgba(201, 167, 81, 0.15);
}
.rango-selector {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
flex-shrink: 0;
}
.rango-label {
font-family: 'Qanelas', var(--font-sans);
font-size: 0.75rem;
color: var(--theme-texto);
}
...
...
@@ -2587,24 +3083,31 @@
.rango-btn {
padding: 0.375rem 0.75rem;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.6875rem;
font-
family: 'DM Mono', monospace
;
font-
weight: 500
;
color: var(--theme-texto);
background: transparent;
border:
none
;
border:
1px solid transparent
;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.rango-btn:hover {
color: var(--theme-t
ext
o);
background:
rgba(255,255,255,0.05
);
color: var(--theme-t
itul
o);
background:
var(--theme-surface-hover
);
}
.rango-btn.active {
color: var(--theme-titulo);
background: rgba(255,255,255,0.1);
background: #e8e8e8;
border-color: transparent;
font-weight: 600;
}
:global(html.dark) .rango-btn.active {
background: rgba(255, 255, 255, 0.15);
}
.dropdown-item:disabled {
...
...
@@ -2648,63 +3151,118 @@
font-size: 0.75rem;
color: var(--theme-texto);
flex-shrink: 0;
padding: 0.5rem 0;
}
.chart-comparar {
gap: 1.5rem;
justify-content: center;
height: calc(100vh - 380px);
min-height: 140px;
max-height: 280px;
.chart-comparar-wrapper {
position: relative;
width: 100%;
flex: 1;
min-height: 180px;
}
.chart-comparar-inner {
position: absolute;
top: 10px;
bottom: 30px;
left: 50px;
right: 10px;
}
.chart-comparar + .chart-legend + .kpis-comparar {
margin-top: 0;
.y-label-comparar {
position: absolute;
left: -50px;
width: 45px;
text-align: right;
transform: translateY(50%);
font-family: 'Qanelas', var(--font-sans);
font-size: 0.6875rem;
color: var(--theme-texto);
white-space: nowrap;
}
.bar-group {
.grid-line-comparar {
position: absolute;
left: 0;
right: 0;
border-top: 0.5px dotted var(--theme-texto);
opacity: 0.15;
pointer-events: none;
}
.bars-comparar {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
align-items: flex-end;
gap: 3px;
}
.bar-group-comparar {
flex: 1;
height: 100%;
display: flex;
align-items: flex-end;
justify-content: center;
cursor: pointer;
}
.bars-pair {
.bar
-group-comparar .bar
s-pair {
display: flex;
align-items: flex-end;
gap: 4px;
gap: 2px;
height: 100%;
width: 100%;
justify-content: center;
}
.bar-comparar {
flex: 1;
max-width: 24px;
border-radius: 3px 3px 0 0;
min-height: 2px;
transition: opacity 0.15s ease;
}
.bar-a, .bar-b {
width: 20px;
border-radius: 4px 4px 0 0;
min-height: 4px;
background: var(--bar-color);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), filter 0.25s ease;
.bars-comparar:hover .bar-comparar {
opacity: 0.35;
}
.bar-group.active .bar-a,
.bar-group.active .bar-b {
filter: brightness(1.15);
.bar-group-comparar.active .bar-comparar {
opacity: 1;
}
.bar-group {
cursor: pointer;
padding: 0.5rem 0.35rem;
margin: -0.5rem -0.35rem;
border-radius: 6px;
transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1);
.x-axis-comparar {
position: absolute;
bottom: 0;
left: 50px;
right: 10px;
height: 30px;
display: flex;
align-items: center;
}
.bar-group.active {
background: rgba(42, 157, 143, 0.15);
.x-label-comparar {
flex: 1;
text-align: center;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.6875rem;
color: var(--theme-texto);
opacity: 0;
transition: opacity 0.2s ease;
}
.bar-label.highlight {
color: var(--theme-titulo);
font-weight: 500;
transition: color 0.25s ease;
.x-label-comparar.visible {
opacity: 1;
}
.kpis-card-comparar {
background: var(--theme-surface);
border-radius: 16px;
padding: 0.75rem 1.25rem;
}
.chart-legend {
...
...
@@ -2713,12 +3271,14 @@
gap: 2rem;
margin-top: 0.5rem;
margin-bottom: 0.75rem;
flex-shrink: 0;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-family: 'Qanelas', var(--font-sans);
font-size: 0.75rem;
color: var(--theme-texto);
}
...
...
@@ -2729,52 +3289,64 @@
border-radius: 3px;
}
/* KPIs comparativos - tabla */
.kpis-tabla {
background: transparent;
box-shadow: inset 0 0 0 1px rgba(255,255,255,0.08);
border-radius: 10px;
padding: 0.5rem 1rem;
.legend-pending {
opacity: 0.5;
font-style: italic;
}
.comparador-hint-select {
margin-top: 0.75rem;
}
/* KPIs comparativos - tabla */
/* Tabla styles inside kpis-card-comparar */
.tabla-header {
display: grid;
grid-template-columns: 1fr 1
fr 1
fr;
gap:
1
rem;
grid-template-columns: 1fr 1
.2fr 1.2
fr;
gap:
0.75
rem;
padding-bottom: 0.375rem;
border-bottom: 1px solid var(--theme-borde);
margin-bottom: 0.
37
5rem;
margin-bottom: 0.
2
5rem;
}
.tabla-header .tabla-val {
font-family: '
DM Mono', monospace
;
font-family: '
Qanelas', var(--font-sans)
;
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.05em;
text-transform: uppercase;
text-align: center;
}
.tabla-entidad {
font-size: 0.6875rem;
line-height: 1.3;
word-break: break-word;
hyphens: auto;
}
.tabla-row {
display: grid;
grid-template-columns: 1fr 1
fr 1
fr;
gap:
1
rem;
padding: 0.
2
5rem 0;
grid-template-columns: 1fr 1
.2fr 1.2
fr;
gap:
0.75
rem;
padding: 0.
187
5rem 0;
}
.tabla-label {
font-family: '
DM Mono', monospace
;
font-size: 0.75rem;
font-family: '
Qanelas', var(--font-sans)
;
font-size: 0.
68
75rem;
font-weight: 500;
color: var(--theme-texto);
display: flex;
align-items: center;
text-transform: uppercase;
letter-spacing: 0.0
3
em;
letter-spacing: 0.0
5
em;
}
.tabla-val {
font-family: '
DM Mono', monospace
;
font-size: 0.
93
75rem;
font-family: '
Qanelas', var(--font-sans)
;
font-size: 0.
8
75rem;
font-weight: 500;
color: var(--theme-titulo);
text-align: center;
...
...
@@ -2947,6 +3519,16 @@
font-size: 0.45rem;
}
.main-comparar {
padding: 0.75rem;
}
.chart-card-comparar,
.kpis-card-comparar {
padding: 1rem;
border-radius: 12px;
}
.comparador-selectors {
flex-direction: column;
gap: 0.75rem;
...
...
@@ -2956,6 +3538,14 @@
display: none;
}
.comparador-search-wrapper {
max-width: 100%;
}
.chart-container-comparar {
min-height: 180px;
}
/* Ranking responsive */
.ranking-title {
font-size: 1.25rem;
...
...
static/fail.png
0 → 100644
View file @
1d184ff
257 KB
static/problem.png
0 → 100644
View file @
1d184ff
71.8 KB
Please
register
or
login
to post a comment