Feat Widgets Cards y Optimización de Consultas

This commit is contained in:
2025-09-28 19:04:09 -03:00
parent 67634ae947
commit 3b0eee25e6
71 changed files with 5415 additions and 442 deletions

View File

@@ -1,4 +1,6 @@
// src/features/legislativas/rovinciales/DevAppLegislativas.tsx
import { ResultadosNacionalesCardsWidget } from './nacionales/ResultadosNacionalesCardsWidget';
import { CongresoNacionalWidget } from './nacionales/CongresoNacionalWidget';
import { PanelNacionalWidget } from './nacionales/PanelNacionalWidget';
import './DevAppStyle.css'
@@ -6,9 +8,8 @@ export const DevAppLegislativas = () => {
return (
<div className="container">
<h1>Visor de Widgets</h1>
{/* Le pasamos el ID de la elección que queremos visualizar.
Para tus datos de prueba provinciales, este ID es 1. */}
<ResultadosNacionalesCardsWidget eleccionId={2} />
<CongresoNacionalWidget eleccionId={2} />
<PanelNacionalWidget eleccionId={2} />
</div>
);

View File

@@ -0,0 +1,162 @@
// src/features/legislativas/nacionales/CongresoNacionalWidget.tsx
import { useState, Suspense, useMemo } from 'react';
import { useSuspenseQuery } from '@tanstack/react-query';
import { Tooltip } from 'react-tooltip';
import { DiputadosNacionalesLayout } from '../../../components/common/DiputadosNacionalesLayout';
import { SenadoresNacionalesLayout } from '../../../components/common/SenadoresNacionalesLayout';
import { getComposicionNacional, type ComposicionNacionalData, type PartidoComposicionNacional } from '../../../apiService';
import '../provinciales/CongresoWidget.css';
interface CongresoNacionalWidgetProps {
eleccionId: number;
}
const formatTimestamp = (dateString: string) => {
if (!dateString) return '...';
const date = new Date(dateString);
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${day}/${month}/${year} ${hours}:${minutes}`;
};
const WidgetContent = ({ eleccionId }: CongresoNacionalWidgetProps) => {
const [camaraActiva, setCamaraActiva] = useState<'diputados' | 'senadores'>('diputados');
const [isHovering, setIsHovering] = useState(false);
const { data } = useSuspenseQuery<ComposicionNacionalData>({
queryKey: ['composicionNacional', eleccionId],
queryFn: () => getComposicionNacional(eleccionId),
refetchInterval: 30000,
});
const datosCamaraActual = data[camaraActiva];
const partidosOrdenados = useMemo(() => {
if (!datosCamaraActual?.partidos) return [];
const partidosACopiar = [...datosCamaraActual.partidos];
partidosACopiar.sort((a, b) => {
const ordenA = camaraActiva === 'diputados' ? a.ordenDiputadosNacionales : a.ordenSenadoresNacionales;
const ordenB = camaraActiva === 'diputados' ? b.ordenDiputadosNacionales : b.ordenSenadoresNacionales;
return (ordenA ?? 999) - (ordenB ?? 999);
});
return partidosACopiar;
}, [datosCamaraActual, camaraActiva]);
const partyDataParaLayout = useMemo(() => {
if (camaraActiva === 'senadores') return partidosOrdenados;
if (!partidosOrdenados || !datosCamaraActual.presidenteBancada?.color) return partidosOrdenados;
const partidoPresidente = partidosOrdenados.find(p => p.color === datosCamaraActual.presidenteBancada!.color);
if (!partidoPresidente) return partidosOrdenados;
const adjustedPartyData = JSON.parse(JSON.stringify(partidosOrdenados));
const partidoAjustar = adjustedPartyData.find((p: PartidoComposicionNacional) => p.id === partidoPresidente.id);
if (partidoAjustar) {
const tipoBanca = datosCamaraActual.presidenteBancada.tipoBanca;
if (tipoBanca === 'ganada' && partidoAjustar.bancasGanadas > 0) {
partidoAjustar.bancasGanadas -= 1;
} else if (tipoBanca === 'previa' && partidoAjustar.bancasFijos > 0) {
partidoAjustar.bancasFijos -= 1;
} else {
if (partidoAjustar.bancasGanadas > 0) {
partidoAjustar.bancasGanadas -= 1;
} else if (partidoAjustar.bancasFijos > 0) {
partidoAjustar.bancasFijos -= 1;
}
}
}
return adjustedPartyData;
}, [partidosOrdenados, datosCamaraActual.presidenteBancada, camaraActiva]);
return (
<div className="congreso-container">
<div className="congreso-grafico">
<div
className={`congreso-hemiciclo-wrapper ${isHovering ? 'is-hovering' : ''}`}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
{camaraActiva === 'diputados' ?
<DiputadosNacionalesLayout
partyData={partyDataParaLayout}
presidenteBancada={datosCamaraActual.presidenteBancada || null}
size={700}
/> :
<SenadoresNacionalesLayout
partyData={partyDataParaLayout}
presidenteBancada={datosCamaraActual.presidenteBancada || null}
size={700}
/>
}
</div>
<div className="congreso-footer">
<div className="footer-legend">
<div className="footer-legend-item">
{/* Usamos la nueva clase CSS para el círculo sólido */}
<span className="legend-icon legend-icon--solid"></span>
<span>Bancas en juego</span>
</div>
<div className="footer-legend-item">
{/* Reemplazamos el SVG por un span con la nueva clase para el anillo */}
<span className="legend-icon legend-icon--ring"></span>
<span>Bancas previas</span>
</div>
</div>
<div className="footer-timestamp">
Última Actualización: {formatTimestamp(datosCamaraActual.ultimaActualizacion)}
</div>
</div>
</div>
<div className="congreso-summary">
<div className="chamber-tabs">
<button className={camaraActiva === 'diputados' ? 'active' : ''} onClick={() => setCamaraActiva('diputados')}>
Diputados
</button>
<button className={camaraActiva === 'senadores' ? 'active' : ''} onClick={() => setCamaraActiva('senadores')}>
Senadores
</button>
</div>
<h3>{datosCamaraActual.camaraNombre}</h3>
<div className="summary-metric">
<span>Total de Bancas</span>
<strong>{datosCamaraActual.totalBancas}</strong>
</div>
<div className="summary-metric">
<span>Bancas en Juego</span>
<strong>{datosCamaraActual.bancasEnJuego}</strong>
</div>
<hr />
<div className="partido-lista-container">
<ul className="partido-lista">
{partidosOrdenados
.filter(p => p.bancasTotales > 0)
.map((partido: PartidoComposicionNacional) => (
<li key={partido.id}>
<span className="partido-color-box" style={{ backgroundColor: partido.color || '#808080' }}></span>
<span className="partido-nombre">{partido.nombreCorto || partido.nombre}</span>
<strong
className="partido-bancas"
title={`${partido.bancasFijos} bancas previas + ${partido.bancasGanadas} ganadas`}
>
{partido.bancasTotales}
</strong>
</li>
))}
</ul>
</div>
</div>
<Tooltip id="party-tooltip" />
</div>
);
};
export const CongresoNacionalWidget = ({ eleccionId }: CongresoNacionalWidgetProps) => {
return (
<Suspense fallback={<div className="congreso-container loading" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '400px' }}>Cargando composición del congreso...</div>}>
<WidgetContent eleccionId={eleccionId} />
</Suspense>
);
};

View File

@@ -1,10 +1,11 @@
/* src/features/legislativas/nacionales/PanelNaciona.css */
/* src/features/legislativas/nacionales/PanelNacional.css */
.panel-nacional-container {
font-family: 'Roboto', sans-serif;
max-width: 1200px;
margin: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
position: relative;
}
.panel-header {
@@ -491,13 +492,11 @@
/* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */
.mobile-view-toggle {
display: none;
/* Oculto por defecto */
position: fixed;
bottom: 20px;
position: absolute; /* <-- CAMBIO: De 'fixed' a 'absolute' */
bottom: 10px; /* <-- AJUSTE: Menos espacio desde abajo */
left: 50%;
transform: translateX(-50%);
z-index: 100;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 30px;
padding: 5px;

View File

@@ -0,0 +1,259 @@
/* src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.css */
/* --- Variables de Diseño --- */
:root {
--card-border-color: #e0e0e0;
--card-bg-color: #ffffff;
--card-header-bg-color: #f8f9fa;
--card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
--text-primary: #212529;
--text-secondary: #6c757d;
--font-family: "Public Sans", system-ui, sans-serif;
--primary-accent-color: #007bff;
}
/* --- Contenedor Principal del Widget --- */
.cards-widget-container {
font-family: var(--font-family);
width: 100%;
max-width: 1200px;
margin: 2rem auto;
}
.cards-widget-container h2 {
font-size: 1.75rem;
color: var(--text-primary);
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--card-border-color);
}
/* --- Grilla de Tarjetas --- */
.cards-grid {
display: grid;
/* Crea columnas flexibles que se ajustan al espacio disponible */
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 1.5rem;
}
/* --- Tarjeta Individual --- */
.provincia-card {
background-color: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 8px;
box-shadow: var(--card-shadow);
display: flex;
flex-direction: column;
overflow: hidden; /* Asegura que los bordes redondeados se apliquen al contenido */
}
/* --- Cabecera de la Tarjeta --- */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--card-header-bg-color);
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--card-border-color);
}
.header-info h3 {
margin: 0;
font-size: 1.2rem;
font-weight: 700;
color: var(--text-primary);
}
.header-info span {
font-size: 0.8rem;
color: var(--text-secondary);
text-transform: uppercase;
}
.header-map {
width: 90px;
height: 90px;
flex-shrink: 0;
border-radius: 4px;
overflow: hidden;
background-color: #e9ecef;
padding: 0.25rem;
box-sizing: border-box; /* Para que el padding no aumente el tamaño total */
}
/* Contenedor del SVG para asegurar que se ajuste al espacio */
.map-svg-container, .map-placeholder {
width: 100%;
height: 100%;
}
/* Estilo para el SVG renderizado */
.map-svg-container svg {
width: 100%;
height: 100%;
object-fit: contain; /* Asegura que el mapa no se deforme */
}
/* Placeholder para cuando el mapa no carga */
.map-placeholder.error {
background-color: #f8d7da; /* Un color de fondo rojizo para indicar un error */
}
/* --- Cuerpo de la Tarjeta --- */
.card-body {
padding: 0.5rem 1rem;
}
.candidato-row {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0;
border-bottom: 1px solid #f0f0f0;
}
.candidato-row:last-child {
border-bottom: none;
}
.candidato-foto {
width: 45px;
height: 45px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.candidato-data {
flex-grow: 1;
min-width: 0; /* Permite que el texto se trunque si es necesario */
margin-right: 0.5rem;
}
.candidato-nombre {
font-weight: 700;
font-size: 0.95rem;
color: var(--text-primary);
display: block;
}
.candidato-partido {
font-size: 0.75rem;
color: var(--text-secondary);
text-transform: uppercase;
display: block;
margin-bottom: 0.3rem;
}
.progress-bar-container {
height: 6px;
background-color: #e9ecef;
border-radius: 3px;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 3px;
transition: width 0.5s ease-out;
}
.candidato-stats {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
flex-shrink: 0;
padding-left: 0.5rem;
}
.stats-percent {
font-weight: 700;
font-size: 1.1rem;
color: var(--text-primary);
}
.stats-votos {
font-size: 0.8rem;
color: var(--text-secondary);
}
.stats-bancas {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid var(--card-border-color);
border-radius: 6px;
padding: 0.25rem 0.5rem;
margin-left: 0.75rem;
font-weight: 700;
font-size: 1.2rem;
color: var(--primary-accent-color);
min-width: 50px;
}
.stats-bancas span {
font-size: 0.65rem;
font-weight: 500;
color: var(--text-secondary);
text-transform: uppercase;
margin-top: -4px;
}
/* --- Pie de la Tarjeta --- */
.card-footer {
display: grid;
grid-template-columns: repeat(3, 1fr);
background-color: var(--card-header-bg-color);
border-top: 1px solid var(--card-border-color);
padding: 0.75rem 0;
text-align: center;
}
.card-footer div {
border-right: 1px solid var(--card-border-color);
}
.card-footer div:last-child {
border-right: none;
}
.card-footer span {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
}
.card-footer strong {
font-size: 1rem;
font-weight: 700;
color: var(--text-primary);
}
/* --- Media Query para Móvil --- */
@media (max-width: 480px) {
.cards-grid {
/* En pantallas muy pequeñas, forzamos una sola columna */
grid-template-columns: 1fr;
}
.card-header {
padding: 0.5rem;
}
.header-info h3 {
font-size: 1rem;
}
}
/* --- NUEVOS ESTILOS PARA EL NOMBRE DEL PARTIDO CUANDO ES EL TÍTULO PRINCIPAL --- */
.candidato-partido.main-title {
font-size: 0.95rem; /* Hacemos la fuente más grande */
font-weight: 700; /* La ponemos en negrita, como el nombre del candidato */
color: var(--text-primary); /* Usamos el color de texto principal */
text-transform: none; /* Quitamos el 'uppercase' para que se lea mejor */
margin-bottom: 0.3rem;
}

View File

@@ -0,0 +1,30 @@
// src/features/legislativas/nacionales/ResultadosNacionalesCardsWidget.tsx
import { useQuery } from '@tanstack/react-query';
import { getResumenPorProvincia } from '../../../apiService';
import { ProvinciaCard } from './components/ProvinciaCard';
import './ResultadosNacionalesCardsWidget.css';
interface Props {
eleccionId: number;
}
export const ResultadosNacionalesCardsWidget = ({ eleccionId }: Props) => {
const { data, isLoading, error } = useQuery({
queryKey: ['resumenPorProvincia', eleccionId],
queryFn: () => getResumenPorProvincia(eleccionId),
});
if (isLoading) return <div>Cargando resultados por provincia...</div>;
if (error) return <div>Error al cargar los datos.</div>;
return (
<section className="cards-widget-container">
<h2>Resultados elecciones nacionales 2025</h2>
<div className="cards-grid">
{data?.map(provinciaData => (
<ProvinciaCard key={provinciaData.provinciaId} data={provinciaData} />
))}
</div>
</section>
);
};

View File

@@ -0,0 +1,64 @@
// src/features/legislativas/nacionales/components/MiniMapaSvg.tsx
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { useMemo } from 'react';
import { assetBaseUrl } from '../../../../apiService';
interface MiniMapaSvgProps {
provinciaNombre: string;
fillColor: string;
}
// Función para normalizar el nombre de la provincia y que coincida con el nombre del archivo SVG
const normalizarNombreParaUrl = (nombre: string) =>
nombre
.toLowerCase()
.replace(/ /g, '_') // Reemplaza espacios con guiones bajos
.normalize("NFD") // Descompone acentos para eliminarlos en el siguiente paso
.replace(/[\u0300-\u036f]/g, ""); // Elimina los acentos
export const MiniMapaSvg = ({ provinciaNombre, fillColor }: MiniMapaSvgProps) => {
const nombreNormalizado = normalizarNombreParaUrl(provinciaNombre);
// Asumimos que los SVGs están en /public/maps/provincias-svg/
const mapFileUrl = `${assetBaseUrl}/maps/provincias-svg/${nombreNormalizado}.svg`;
// Usamos React Query para fetchear el contenido del SVG como texto
const { data: svgContent, isLoading, isError } = useQuery<string>({
queryKey: ['svgMapa', nombreNormalizado],
queryFn: async () => {
const response = await axios.get(mapFileUrl, { responseType: 'text' });
return response.data;
},
staleTime: Infinity, // Estos archivos son estáticos y no cambian
gcTime: Infinity,
retry: false, // No reintentar si el archivo no existe
});
// Usamos useMemo para modificar el SVG solo cuando el contenido o el color cambian
const modifiedSvg = useMemo(() => {
if (!svgContent) return '';
// Usamos una expresión regular para encontrar todas las etiquetas <path>
// y añadirles el atributo de relleno con el color del ganador.
// Esto sobrescribirá cualquier 'fill' que ya exista en la etiqueta.
return svgContent.replace(/<path/g, `<path fill="${fillColor}"`);
}, [svgContent, fillColor]);
if (isLoading) {
return <div className="map-placeholder" />;
}
if (isError || !modifiedSvg) {
// Muestra un placeholder si el SVG no se encontró o está vacío
return <div className="map-placeholder error" />;
}
// Renderizamos el SVG modificado. dangerouslySetInnerHTML es seguro aquí
// porque el contenido proviene de nuestros propios archivos SVG estáticos.
return (
<div
className="map-svg-container"
dangerouslySetInnerHTML={{ __html: modifiedSvg }}
/>
);
};

View File

@@ -0,0 +1,78 @@
// src/features/legislativas/nacionales/components/ProvinciaCard.tsx
import type { ResumenProvincia } from '../../../../types/types';
import { MiniMapaSvg } from './MiniMapaSvg';
import { ImageWithFallback } from '../../../../components/common/ImageWithFallback';
import { assetBaseUrl } from '../../../../apiService';
interface ProvinciaCardProps {
data: ResumenProvincia;
}
const formatNumber = (num: number) => num.toLocaleString('es-AR');
const formatPercent = (num: number) => `${num.toFixed(2).replace('.', ',')}%`;
export const ProvinciaCard = ({ data }: ProvinciaCardProps) => {
// Determinamos el color del ganador para pasárselo al mapa.
// Si no hay ganador, usamos un color gris por defecto.
const colorGanador = data.resultados[0]?.color || '#d1d1d1';
return (
<div className="provincia-card">
<header className="card-header">
<div className="header-info">
<h3 style={{ whiteSpace: 'normal' }}>{data.provinciaNombre}</h3>
<span>DIPUTADOS NACIONALES</span>
</div>
<div className="header-map">
<MiniMapaSvg provinciaNombre={data.provinciaNombre} fillColor={colorGanador} />
</div>
</header>
<div className="card-body">
{data.resultados.map(res => (
<div key={res.agrupacionId} className="candidato-row">
<ImageWithFallback src={res.fotoUrl ?? undefined} fallbackSrc={`${assetBaseUrl}/default-avatar.png`} alt={res.nombreCandidato ?? res.nombreAgrupacion} className="candidato-foto" />
<div className="candidato-data">
{res.nombreCandidato && (
<span className="candidato-nombre">{res.nombreCandidato}</span>
)}
<span className={`candidato-partido ${!res.nombreCandidato ? 'main-title' : ''}`}>
{res.nombreAgrupacion}
</span>
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${res.porcentaje}%`, backgroundColor: res.color || '#ccc' }} />
</div>
</div>
<div className="candidato-stats">
<span className="stats-percent">{formatPercent(res.porcentaje)}</span>
<span className="stats-votos">{formatNumber(res.votos)} votos</span>
</div>
<div className="stats-bancas">
+{res.bancasObtenidas}
<span>Bancas</span>
</div>
</div>
))}
</div>
<footer className="card-footer">
<div>
<span>Participación</span>
{/* Usamos los datos reales del estado de recuento */}
<strong>{formatPercent(data.estadoRecuento?.participacionPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Mesas escrutadas</span>
<strong>{formatPercent(data.estadoRecuento?.mesasTotalizadasPorcentaje ?? 0)}</strong>
</div>
<div>
<span>Votos totales</span>
{/* Usamos el nuevo campo cantidadVotantes */}
<strong>{formatNumber(data.estadoRecuento?.cantidadVotantes ?? 0)}</strong>
</div>
</footer>
</div>
);
};

View File

@@ -1,27 +1,35 @@
/* src/features/legislativas/provinciales/CongresoWidget.css */
.congreso-container {
display: flex;
/* Se reduce ligeramente el espacio entre el gráfico y el panel */
gap: 1rem;
gap: 1.5rem;
background-color: #ffffff;
border: 1px solid #e0e0e0;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
padding: 1rem;
border-radius: 8px;
max-width: 800px;
max-width: 900px;
margin: 20px auto;
font-family: "Public Sans", system-ui, sans-serif;
color: #333333;
align-items: center;
}
.congreso-grafico {
/* --- CAMBIO PRINCIPAL: Se aumenta la proporción del gráfico --- */
flex: 1 1 65%;
flex: 2;
min-width: 300px;
display: flex;
flex-direction: column;
}
.congreso-hemiciclo-wrapper {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) {
opacity: 0.4;
}
.congreso-grafico svg {
@@ -30,35 +38,139 @@
animation: fadeIn 0.8s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
/* --- NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */
.congreso-footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem 0 1rem;
margin-top: auto; /* Empuja el footer a la parte inferior del contenedor flex */
font-size: 0.8em;
color: #666;
border-top: 1px solid #eee;
}
to {
opacity: 1;
transform: scale(1);
}
.footer-legend {
display: flex;
gap: 1.5rem; /* Espacio entre los items de la leyenda */
}
.footer-legend-item {
display: flex;
align-items: center;
gap: 0.5rem; /* Espacio entre el icono y el texto */
}
.footer-timestamp {
font-weight: 500;
}
/* --- ESTILOS PARA HOVER --- */
/* Estilo base para cada círculo de escaño */
.seat-circle {
transition: all 0.2s ease-in-out;
}
.party-block {
cursor: pointer;
transition: opacity 0.2s ease-in-out;
}
.party-block:hover .seat-circle {
stroke: #333 !important; /* Borde oscuro para resaltar */
stroke-width: 1.5px !important;
stroke-opacity: 1;
filter: brightness(1.1);
}
/* CORRECCIÓN: El selector ahora apunta al wrapper correcto */
.congreso-hemiciclo-wrapper.is-hovering .party-block:not(:hover) {
opacity: 0.3; /* Hacemos el desvanecimiento más pronunciado */
}
.congreso-grafico svg {
width: 100%;
height: auto;
animation: fadeIn 0.8s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
/* --- INICIO DE NUEVOS ESTILOS PARA EL FOOTER DEL GRÁFICO --- */
.congreso-footer {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 0.5rem 0 0.5rem;
margin-top: auto;
font-size: 0.8em;
color: #666;
border-top: 1px solid #eee;
}
.footer-legend {
display: flex;
gap: 1.25rem;
align-items: center;
}
.footer-legend-item {
display: flex;
align-items: center;
gap: 0.6rem;
font-size: 1.1em;
}
/* Creamos una clase base para ambos iconos para compartir tamaño */
.legend-icon {
display: inline-block;
width: 14px; /* Tamaño base para ambos iconos */
height: 14px;
border-radius: 50%;
box-sizing: border-box;
}
/* Estilo para el icono de "Bancas en juego" (círculo sólido) */
.legend-icon--solid {
background-color: #888;
border: 1px solid #777;
}
/* Estilo para el icono de "Bancas previas" (anillo translúcido) */
.legend-icon--ring {
background-color: rgba(136, 136, 136, 0.3); /* #888 con opacidad */
border: 1px solid #888; /* Borde sólido del mismo color */
}
.footer-timestamp {
font-weight: 500;
font-size: 0.75em;
}
.congreso-summary {
/* --- CAMBIO PRINCIPAL: Se reduce la proporción del panel de datos --- */
flex: 1 1 35%;
flex: 1;
border-left: 1px solid #e0e0e0;
/* Se reduce el padding para dar aún más espacio al gráfico */
padding-left: 1rem;
padding-left: 1.25rem; /* Un poco más de padding */
display: flex;
flex-direction: column;
justify-content: flex-start;
}
.congreso-summary h3 {
margin-top: 0;
margin-bottom: 0.75rem; /* Margen inferior reducido */
font-size: 1.4em;
color: #212529;
}
.chamber-tabs {
display: flex;
margin-bottom: 1.5rem;
margin-bottom: 1rem; /* Margen inferior reducido */
border: 1px solid #dee2e6;
border-radius: 6px;
overflow: hidden;
@@ -66,7 +178,7 @@
.chamber-tabs button {
flex: 1;
padding: 0.75rem 0.5rem;
padding: 0.5rem 0.5rem;
border: none;
background-color: #f8f9fa;
color: #6c757d;
@@ -94,7 +206,7 @@
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
margin-bottom: 0.25rem; /* Margen inferior muy reducido */
font-size: 1.1em;
}
@@ -107,7 +219,15 @@
.congreso-summary hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 1.5rem 0;
margin: 1rem 0; /* Margen vertical reducido */
}
/* Contenedor de la lista de partidos para aplicar el scroll */
.partido-lista-container {
flex-grow: 1; /* Ocupa el espacio vertical disponible */
overflow-y: auto; /* Muestra el scrollbar si es necesario */
min-height: 0; /* Truco de Flexbox para que el scroll funcione */
padding-right: 8px; /* Espacio para el scrollbar */
}
.partido-lista {
@@ -119,14 +239,14 @@
.partido-lista li {
display: flex;
align-items: center;
margin-bottom: 0.75rem;
margin-bottom: 0.85rem; /* Un poco más de espacio entre items */
}
.partido-color-box {
width: 14px;
height: 14px;
border-radius: 3px;
margin-right: 10px;
width: 16px; /* Cuadro de color más grande */
height: 16px;
border-radius: 4px; /* Un poco más cuadrado */
margin-right: 12px;
flex-shrink: 0;
}
@@ -139,19 +259,54 @@
font-size: 1.1em;
}
/* --- Media Query para Responsividad Móvil --- */
/* --- Media Query para Responsividad Móvil (HASTA 768px) --- */
@media (max-width: 768px) {
.congreso-container {
flex-direction: column;
padding: 1.5rem;
padding: 0.5rem;
height: auto;
max-height: none;
}
.congreso-summary {
border-left: none;
padding-left: 0;
margin-top: 2rem;
border-top: 1px solid #e0e0e0;
padding-top: 1.5rem;
}
.partido-lista-container {
overflow-y: visible;
max-height: none;
}
.congreso-footer {
flex-direction: column; /* Apila la leyenda y el timestamp verticalmente */
align-items: flex-start; /* Alinea todo a la izquierda */
gap: 0.5rem; /* Añade un pequeño espacio entre la leyenda y el timestamp */
padding: 0.75rem 0rem; /* Ajusta el padding para móvil */
align-items: center;
}
.footer-legend {
gap: 0.75rem; /* Reduce el espacio entre los items de la leyenda */
}
.footer-legend-item{
font-size: 1em;
}
.footer-timestamp {
font-size: 0.75em; /* Reduce el tamaño de la fuente para que quepa mejor */
}
}
/* --- Media Query para Escritorio (DESDE 769px en adelante) --- */
@media (min-width: 769px) {
.congreso-container {
flex-direction: row;
align-items: stretch;
height: 500px;
}
}