Refinamiento de Funciones y Estética de Mapa
This commit is contained in:
		| @@ -5,7 +5,7 @@ import './DevAppStyle.css' | ||||
| export const DevAppLegislativas = () => { | ||||
|     return ( | ||||
|         <div className="container"> | ||||
|             <h1>Il visualizzatore di widget - Elecciones Nacionales 2025</h1> | ||||
|             <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. */} | ||||
|   | ||||
| @@ -10,6 +10,12 @@ | ||||
| .panel-header { | ||||
|   padding: 1rem 1.5rem; | ||||
|   border-bottom: 1px solid #e0e0e0; | ||||
|   position: relative; | ||||
|   /* Necesario para que z-index funcione */ | ||||
|   z-index: 20; | ||||
|   /* Un número alto para ponerlo al frente */ | ||||
|   background-color: white; | ||||
|   /* Asegura que no sea transparente */ | ||||
| } | ||||
|  | ||||
| /* Contenedor para alinear título y selector */ | ||||
| @@ -29,20 +35,89 @@ | ||||
|   min-width: 220px; | ||||
| } | ||||
|  | ||||
| /* El contenedor principal del selector (la parte visible antes de hacer clic) */ | ||||
| .categoria-selector__control { | ||||
|   border-radius: 8px !important; | ||||
|   /* Coincide con el radio de los otros elementos */ | ||||
|   border: 1px solid #e0e0e0 !important; | ||||
|   box-shadow: none !important; | ||||
|   /* Quitamos la sombra por defecto */ | ||||
|   transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| /* Estilo cuando el selector está enfocado (seleccionado) */ | ||||
| .categoria-selector__control--is-focused { | ||||
|   border-color: #007bff !important; | ||||
|   box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25) !important; | ||||
| } | ||||
|  | ||||
| /* El texto del valor seleccionado */ | ||||
| .categoria-selector__single-value { | ||||
|   font-weight: 500; | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| /* El menú desplegable que contiene las opciones */ | ||||
| .categoria-selector__menu { | ||||
|   border-radius: 8px !important; | ||||
|   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important; | ||||
|   border: 1px solid #e0e0e0 !important; | ||||
|   margin-top: 4px !important; | ||||
|   /* Pequeño espacio entre el control y el menú */ | ||||
| } | ||||
|  | ||||
| /* Cada una de las opciones en la lista */ | ||||
| .categoria-selector__option { | ||||
|   cursor: pointer; | ||||
|   transition: background-color 0.2s, color 0.2s; | ||||
| } | ||||
|  | ||||
| /* Estilo de una opción cuando pasas el mouse por encima (estado 'focused') */ | ||||
| .categoria-selector__option--is-focused { | ||||
|   background-color: #f0f8ff; | ||||
|   /* Un azul muy claro */ | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| /* Estilo de la opción que está actualmente seleccionada */ | ||||
| .categoria-selector__option--is-selected { | ||||
|   background-color: #007bff; | ||||
|   color: white; | ||||
| } | ||||
|  | ||||
| /* La pequeña línea vertical que separa el contenido del indicador (la flecha) */ | ||||
| .categoria-selector__indicator-separator { | ||||
|   display: none; | ||||
|   /* La ocultamos para un look más limpio */ | ||||
| } | ||||
|  | ||||
| /* El indicador (la flecha hacia abajo) */ | ||||
| .categoria-selector__indicator { | ||||
|   color: #a0a0a0; | ||||
|   transition: color 0.2s; | ||||
| } | ||||
|  | ||||
| .categoria-selector__indicator:hover { | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| /* --- ESTILOS MODERNOS PARA BREADCRUMBS --- */ | ||||
|  | ||||
| .breadcrumbs-container { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.5rem; /* Espacio entre elementos */ | ||||
|   font-size: 0.9rem; | ||||
|   gap: 0.5rem; | ||||
|   /* Espacio entre elementos */ | ||||
|   font-size: 1rem; | ||||
| } | ||||
|  | ||||
| .breadcrumb-item, .breadcrumb-item-actual { | ||||
| .breadcrumb-item, | ||||
| .breadcrumb-item-actual { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   padding: 0.4rem 0.8rem; | ||||
|   border-radius: 8px; /* Bordes redondeados para efecto píldora */ | ||||
|   border-radius: 8px; | ||||
|   /* Bordes redondeados para efecto píldora */ | ||||
|   transition: background-color 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| @@ -62,7 +137,8 @@ | ||||
| .breadcrumb-item-actual { | ||||
|   background-color: transparent; | ||||
|   color: #000; | ||||
|   font-weight: 700; /* Más peso para el nivel actual */ | ||||
|   font-weight: 700; | ||||
|   /* Más peso para el nivel actual */ | ||||
| } | ||||
|  | ||||
| .breadcrumb-icon { | ||||
| @@ -71,7 +147,8 @@ | ||||
| } | ||||
|  | ||||
| .breadcrumb-separator { | ||||
|   color: #a0a0a0; /* Color sutil para el separador */ | ||||
|   color: #a0a0a0; | ||||
|   /* Color sutil para el separador */ | ||||
|   font-size: 1.2rem; | ||||
| } | ||||
|  | ||||
| @@ -85,18 +162,21 @@ | ||||
|  | ||||
| /* Columna del mapa */ | ||||
| .mapa-column { | ||||
|   flex: 2; /* Por defecto, ocupa 2/3 del espacio */ | ||||
|   flex: 2; | ||||
|   /* Por defecto, ocupa 2/3 del espacio */ | ||||
|   position: relative; | ||||
|   transition: flex 0.5s ease-in-out; | ||||
| } | ||||
|  | ||||
| /* Columna de resultados */ | ||||
| .resultados-column { | ||||
|   flex: 1; /* Por defecto, ocupa 1/3 */ | ||||
|   flex: 1; | ||||
|   /* Por defecto, ocupa 1/3 */ | ||||
|   overflow-y: auto; | ||||
|   padding: 1.5rem; | ||||
|   transition: all 0.5s ease-in-out; | ||||
|   min-width: 320px; /* Un ancho mínimo para que no se comprima demasiado */ | ||||
|   min-width: 320px; | ||||
|   /* Un ancho mínimo para que no se comprima demasiado */ | ||||
| } | ||||
|  | ||||
| /* --- NUEVO LAYOUT PARA TARJETAS DE PARTIDO --- */ | ||||
| @@ -105,8 +185,8 @@ | ||||
|   align-items: center; | ||||
|   gap: 1rem; | ||||
|   padding: 1rem 0; | ||||
|   border-bottom: 1px solid #f0f0f0; /* Separador sutil */ | ||||
|   border-left: 5px solid; /* El color se aplica inline */ | ||||
|   border-bottom: 1px solid #f0f0f0; | ||||
|   border-left: 5px solid; | ||||
|   padding-left: 1rem; | ||||
| } | ||||
|  | ||||
| @@ -125,15 +205,22 @@ | ||||
|  | ||||
| .partido-main-content { | ||||
|   flex-grow: 1; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 0.5rem; /* Espacio entre la fila superior y la barra */ | ||||
|   display: grid; | ||||
|   /* CAMBIO: De flex a grid */ | ||||
|   grid-template-columns: 1fr auto; | ||||
|   /* Columna 1 (nombre) flexible, Columna 2 (stats) se ajusta al contenido */ | ||||
|   grid-template-rows: auto auto; | ||||
|   /* Dos filas: una para la info, otra para la barra */ | ||||
|   align-items: center; | ||||
|   /* Alinea verticalmente el contenido de ambas filas */ | ||||
|   gap: 0.25rem 1rem; | ||||
|   /* Espacio entre filas y columnas (0.25rem vertical, 1rem horizontal) */ | ||||
| } | ||||
|  | ||||
| .partido-top-row { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: flex-start; /* Alinea los elementos al tope */ | ||||
|   /* Hacemos que este contenedor sea "invisible" para el grid,  | ||||
|      promoviendo a sus hijos (info y stats) a la cuadrícula principal. */ | ||||
|   display: contents; | ||||
| } | ||||
|  | ||||
| .partido-info-wrapper { | ||||
| @@ -142,7 +229,7 @@ | ||||
| } | ||||
|  | ||||
| .partido-nombre { | ||||
|   font-weight: 500; | ||||
|   font-weight: 800; | ||||
|   white-space: nowrap; | ||||
|   overflow: hidden; | ||||
|   text-overflow: ellipsis; | ||||
| @@ -156,7 +243,8 @@ | ||||
| .partido-stats { | ||||
|   flex-shrink: 0; | ||||
|   text-align: right; | ||||
|   padding-left: 1rem; /* Espacio para que no se pegue al nombre */ | ||||
|   padding-left: 1rem; | ||||
|   /* Ya no necesita ser un contenedor flex, el grid lo posiciona */ | ||||
| } | ||||
|  | ||||
| .partido-porcentaje { | ||||
| @@ -175,6 +263,8 @@ | ||||
|   height: 20px; | ||||
|   background-color: #f0f0f0; | ||||
|   border-radius: 4px; | ||||
|   grid-column: 1 / 3; | ||||
|   /* Le indicamos que ocupe ambas columnas (de la línea 1 a la 3) */ | ||||
| } | ||||
|  | ||||
| .partido-barra-foreground { | ||||
| @@ -182,6 +272,7 @@ | ||||
|   border-radius: 4px; | ||||
|   transition: width 0.5s ease-in-out; | ||||
| } | ||||
|  | ||||
| /* ------------------------------------------- */ | ||||
|  | ||||
| .panel-estado-recuento { | ||||
| @@ -212,13 +303,15 @@ | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| .mapa-render-area { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
| } | ||||
|  | ||||
| .mapa-volver-btn { | ||||
|   position: absolute; | ||||
|   top: 10px; | ||||
| @@ -231,12 +324,15 @@ | ||||
|   cursor: pointer; | ||||
|   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
|  | ||||
| .rsm-zoomable-group { | ||||
|     transition: transform 0.75s ease-in-out; | ||||
|   transition: transform 0.75s ease-in-out; | ||||
| } | ||||
|  | ||||
| .panel-main-content.panel-collapsed .mapa-column { | ||||
|   flex: 1 1 100%; | ||||
| } | ||||
|  | ||||
| .panel-main-content.panel-collapsed .resultados-column { | ||||
|   flex-basis: 0; | ||||
|   min-width: 0; | ||||
| @@ -244,6 +340,7 @@ | ||||
|   padding: 0; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| .panel-toggle-btn { | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
| @@ -256,107 +353,435 @@ | ||||
|   background-color: white; | ||||
|   border-radius: 4px 0 0 4px; | ||||
|   cursor: pointer; | ||||
|   font-size: 1.5rem; | ||||
|   font-size: 1.3rem; | ||||
|   font-weight: bold; | ||||
|   color: #555; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   box-shadow: -2px 0 5px rgba(0,0,0,0.1); | ||||
|   box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); | ||||
|   transition: background-color 0.2s; | ||||
| } | ||||
|  | ||||
| .panel-toggle-btn:hover { | ||||
|   background-color: #f0f0f0; | ||||
| } | ||||
|  | ||||
| .rsm-geography { | ||||
|     cursor: pointer; | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.25px; | ||||
|     outline: none; | ||||
|     transition: filter 0.2s ease-in-out; | ||||
|   cursor: pointer; | ||||
|   stroke: #000000; | ||||
|   stroke-width: 0.25px; | ||||
|   outline: none; | ||||
|   transition: filter 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .rsm-geography:not(.selected):hover { | ||||
|     filter: brightness(1.15); | ||||
|     stroke: #ffffff; | ||||
|     stroke-width: 0.25px; | ||||
|   filter: brightness(1.25); /* Mantenemos el brillo */ | ||||
|   stroke: #ffffff;      /* Color del borde a blanco */ | ||||
|   stroke-width: 0.25px; | ||||
|   paint-order: stroke;  /* Asegura que el borde se dibuje encima del relleno */ | ||||
| } | ||||
|  | ||||
| .rsm-geography.selected { | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.25px; | ||||
|     filter: none; | ||||
|     pointer-events: none; | ||||
|   stroke: #000000; | ||||
|   stroke-width: 0.25px; | ||||
|   filter: none; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .rsm-geography-faded, | ||||
| .rsm-geography-faded-municipality { | ||||
|     opacity: 0.5; | ||||
|     pointer-events: none; | ||||
|   opacity: 0.5; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .caba-comuna-geography { | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.05px; | ||||
|   stroke: #000000; | ||||
|   stroke-width: 0.05px; | ||||
| } | ||||
|  | ||||
| .caba-comuna-geography:not(.selected):hover { | ||||
|     stroke: #000000; | ||||
|     stroke-width: 0.055px; | ||||
|     filter: brightness(1.25); | ||||
|   stroke: #000000; | ||||
|   stroke-width: 0.055px; | ||||
|   filter: brightness(1.25); | ||||
| } | ||||
|  | ||||
| .transition-spinner { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     background-color: rgba(255, 255, 255, 0.5); | ||||
|     z-index: 20; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   background-color: rgba(255, 255, 255, 0.5); | ||||
|   z-index: 20; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .transition-spinner::after { | ||||
|     content: ''; | ||||
|     width: 50px; | ||||
|     height: 50px; | ||||
|     border: 5px solid rgba(0, 0, 0, 0.2); | ||||
|     border-top-color: #007bff; | ||||
|     border-radius: 50%; | ||||
|     animation: spin 1s linear infinite; | ||||
|   content: ''; | ||||
|   width: 50px; | ||||
|   height: 50px; | ||||
|   border: 5px solid rgba(0, 0, 0, 0.2); | ||||
|   border-top-color: #007bff; | ||||
|   border-radius: 50%; | ||||
|   animation: spin 1s linear infinite; | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|     to { transform: rotate(360deg); } | ||||
|   to { | ||||
|     transform: rotate(360deg); | ||||
|   } | ||||
| } | ||||
|  | ||||
| .caba-magnifier-container { | ||||
|     position: absolute; | ||||
|     height: auto; | ||||
|     transform: translate(-50%, -50%); | ||||
|     pointer-events: none; | ||||
|   position: absolute; | ||||
|   height: auto; | ||||
|   transform: translate(-50%, -50%); | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .caba-lupa-svg { | ||||
|     width: 100%; | ||||
|     height: auto; | ||||
|     pointer-events: none; | ||||
|   width: 100%; | ||||
|   height: auto; | ||||
|   pointer-events: none; | ||||
| } | ||||
|  | ||||
| .caba-lupa-interactive-area { | ||||
|     pointer-events: all; | ||||
|     cursor: pointer; | ||||
|     filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.25)); | ||||
|     transition: transform 0.2s ease-in-out; | ||||
|   pointer-events: all; | ||||
|   cursor: pointer; | ||||
|   filter: drop-shadow(0px 2px 4px rgba(0, 0, 0, 0.25)); | ||||
|   transition: transform 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .caba-lupa-interactive-area:hover { | ||||
|     filter: brightness(1.15); | ||||
|     stroke: #ffffff; | ||||
|     stroke-width: 0.25px; | ||||
|   filter: brightness(1.15); | ||||
|   stroke: #ffffff; | ||||
|   stroke-width: 0.25px; | ||||
| } | ||||
|  | ||||
| .skeleton-fila div { | ||||
|   background: #f6f7f8; | ||||
|   background-image: linear-gradient(to right, #f6f7f8 0%, #edeef1 20%, #f6f7f8 40%, #f6f7f8 100%); | ||||
|   background-repeat: no-repeat; | ||||
|   background-size: 800px 104px;  | ||||
|   background-size: 800px 104px; | ||||
|   animation: shimmer 1s linear infinite; | ||||
|   border-radius: 4px; | ||||
| } | ||||
|  | ||||
| .skeleton-logo { width: 65px; height: 65px; } | ||||
| .skeleton-text { height: 1em; } | ||||
| .skeleton-bar { height: 20px; margin-top: 4px; } | ||||
| .skeleton-logo { | ||||
|   width: 65px; | ||||
|   height: 65px; | ||||
| } | ||||
|  | ||||
| .skeleton-text { | ||||
|   height: 1em; | ||||
| } | ||||
|  | ||||
| .skeleton-bar { | ||||
|   height: 20px; | ||||
|   margin-top: 4px; | ||||
| } | ||||
|  | ||||
| /* --- NUEVOS ESTILOS PARA EL TOGGLE MÓVIL --- */ | ||||
| .mobile-view-toggle { | ||||
|   display: none; | ||||
|   /* Oculto por defecto */ | ||||
|   position: fixed; | ||||
|   bottom: 20px; | ||||
|   left: 50%; | ||||
|   transform: translateX(-50%); | ||||
|   z-index: 100; | ||||
|  | ||||
|   background-color: rgba(255, 255, 255, 0.9); | ||||
|   border-radius: 30px; | ||||
|   padding: 5px; | ||||
|   box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); | ||||
|   gap: 5px; | ||||
|   backdrop-filter: blur(5px); | ||||
|   -webkit-backdrop-filter: blur(5px); | ||||
| } | ||||
|  | ||||
| .mobile-view-toggle .toggle-btn { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 8px; | ||||
|   padding: 10px 20px; | ||||
|   border: none; | ||||
|   background-color: transparent; | ||||
|   border-radius: 25px; | ||||
|   cursor: pointer; | ||||
|   font-size: 1rem; | ||||
|   font-weight: 500; | ||||
|   color: #555; | ||||
|   transition: all 0.2s ease-in-out; | ||||
| } | ||||
|  | ||||
| .mobile-view-toggle .toggle-btn.active { | ||||
|   background-color: #007bff; | ||||
|   color: white; | ||||
| } | ||||
|  | ||||
| /* --- ESTILOS PARA LOS BOTONES DE ZOOM DEL MAPA --- */ | ||||
| .zoom-controls-container { | ||||
|   position: absolute; | ||||
|   top: 5px; | ||||
|   right: 10px; | ||||
|   z-index: 30; | ||||
|   /* Debe ser MAYOR que el z-index del header (20) */ | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 5px; | ||||
| } | ||||
|  | ||||
| .zoom-btn { | ||||
|   width: 40px; | ||||
|   height: 40px; | ||||
|   background-color: white; | ||||
|   border: 1px solid #ccc; | ||||
|   border-radius: 8px; | ||||
|   box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
|   cursor: pointer; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   font-size: 1.2rem; | ||||
|   transition: background-color 0.2s; | ||||
| } | ||||
|  | ||||
| .zoom-icon-wrapper { | ||||
|   /* Contenedor del icono */ | ||||
|   display: flex; | ||||
|   /* Necesario para que el SVG interno se alinee */ | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
| } | ||||
|  | ||||
| .zoom-icon-wrapper svg { | ||||
|   /* Apunta directamente al SVG del icono */ | ||||
|   width: 20px; | ||||
|   height: 20px; | ||||
|   color: #333; | ||||
| } | ||||
|  | ||||
| .zoom-btn.disabled { | ||||
|   opacity: 0.5; | ||||
|   /* Lo hace semitransparente */ | ||||
|   cursor: not-allowed; | ||||
|   /* Muestra el cursor de "no permitido" */ | ||||
| } | ||||
|  | ||||
| .zoom-btn:hover { | ||||
|   background-color: #f0f0f0; | ||||
| } | ||||
|  | ||||
| /* --- ESTILOS DE CURSOR PARA EL ARRASTRE DEL MAPA --- */ | ||||
| .map-locked .rsm-geography { | ||||
|   cursor: pointer; | ||||
|   /* Cursor normal de clic */ | ||||
| } | ||||
|  | ||||
| .map-pannable .rsm-geography { | ||||
|   cursor: grab; | ||||
|   /* Indica que el mapa se puede arrastrar */ | ||||
| } | ||||
|  | ||||
| .map-pannable .rsm-geography:active { | ||||
|   cursor: grabbing; | ||||
|   /* Indica que se está arrastrando */ | ||||
| } | ||||
|  | ||||
| /* --- MEDIA QUERY PARA RESPONSIVE (ENFOQUE FINAL CON CAPAS) --- */ | ||||
| @media (max-width: 800px) { | ||||
|  | ||||
|   /* --- CONFIGURACIÓN GENERAL --- */ | ||||
|   html, | ||||
|   body { | ||||
|     width: 100%; | ||||
|     overflow-x: hidden; | ||||
|   } | ||||
|  | ||||
|   /* Controles de vista y header (sin cambios) */ | ||||
|   .mobile-view-toggle { | ||||
|     display: flex; | ||||
|   } | ||||
|  | ||||
|   .panel-toggle-btn { | ||||
|     display: none; | ||||
|   } | ||||
|  | ||||
|   .header-top-row { | ||||
|     flex-direction: column; | ||||
|     align-items: flex-start; | ||||
|     gap: 1rem; | ||||
|   } | ||||
|  | ||||
|   .categoria-selector { | ||||
|     width: 100%; | ||||
|   } | ||||
|  | ||||
|   /* --- NUEVO LAYOUT DE CAPAS SUPERPUESTAS --- */ | ||||
|  | ||||
|   /* 1. El contenedor principal ahora es un ancla de posicionamiento */ | ||||
|   .panel-main-content { | ||||
|     position: relative; | ||||
|     /* Clave para que los hijos se posicionen dentro de él */ | ||||
|     height: calc(100vh - 200px); | ||||
|     /* Le damos una altura fija y predecible */ | ||||
|     min-height: 450px; | ||||
|   } | ||||
|  | ||||
|   /* 2. Ambas columnas son capas que ocupan el 100% del espacio del padre */ | ||||
|   .mapa-column, | ||||
|   .resultados-column { | ||||
|     position: absolute; | ||||
|     left: 0; | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
|     box-sizing: border-box; | ||||
|     transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out; | ||||
|   } | ||||
|  | ||||
|   /* Le damos un estilo específico a la columna del mapa para subirla */ | ||||
|   .mapa-column { | ||||
|     top: -50px; | ||||
|     left: -10px; | ||||
|     z-index: 10; | ||||
|   } | ||||
|  | ||||
|  | ||||
|   /* Hacemos que la columna de resultados pueda tener su propio scroll... */ | ||||
|   .resultados-column { | ||||
|     top: 0; | ||||
|     /* Aseguramos que los resultados se queden en su sitio */ | ||||
|     padding: 1rem; | ||||
|     overflow-y: auto; | ||||
|     z-index: 15; | ||||
|   } | ||||
|  | ||||
|   /* 3. Lógica de visibilidad: controlamos qué capa está "arriba" */ | ||||
|   .panel-main-content.mobile-view-mapa .resultados-column { | ||||
|     opacity: 0; | ||||
|     visibility: hidden; | ||||
|     /* Esta es la propiedad clave que ya tenías, pero es importante verificarla */ | ||||
|     pointer-events: none; | ||||
|     /* Asegura que la capa oculta no bloquee el mapa */ | ||||
|   } | ||||
|  | ||||
|   .panel-main-content.mobile-view-resultados .mapa-column { | ||||
|     opacity: 0; | ||||
|     visibility: hidden; | ||||
|     pointer-events: none; | ||||
|   } | ||||
|  | ||||
|   /* Hacemos que la columna de resultados pueda tener su propio scroll si el contenido es largo */ | ||||
|   .resultados-column { | ||||
|     padding: 1rem; | ||||
|     overflow-y: auto; | ||||
|   } | ||||
|  | ||||
|   /* 4. Estilos de los resultados (ya estaban bien, se mantienen) */ | ||||
|   .partido-fila { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 1rem; | ||||
|   } | ||||
|  | ||||
|   .partido-logo { | ||||
|     width: 60px; | ||||
|     height: 60px; | ||||
|     flex-shrink: 0; | ||||
|   } | ||||
|  | ||||
|   .partido-main-content { | ||||
|     flex-grow: 1; | ||||
|     min-width: 0; | ||||
|   } | ||||
|  | ||||
|   .partido-top-row { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: flex-start; | ||||
|   } | ||||
|  | ||||
|   .partido-info-wrapper { | ||||
|     min-width: 0; | ||||
|   } | ||||
|  | ||||
|   .partido-nombre { | ||||
|     white-space: nowrap; | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|   } | ||||
|  | ||||
|   .partido-stats { | ||||
|     text-align: right; | ||||
|     flex-shrink: 0; | ||||
|     padding-left: 0.5rem; | ||||
|   } | ||||
|  | ||||
|   /* --- AJUSTE DE TAMAÑO DEL CONTENEDOR INTERNO DEL MAPA --- */ | ||||
|   .mapa-column .mapa-componente-container, | ||||
|   .mapa-column .mapa-render-area { | ||||
|     height: 100%; | ||||
|   } | ||||
|  | ||||
|   /* Margen de seguridad para el último elemento de la lista de resultados */ | ||||
|   .panel-partidos-container .partido-fila:last-child { | ||||
|     margin-bottom: 90px; | ||||
|   } | ||||
|  | ||||
|   .zoom-controls-container { | ||||
|     top: 55px; | ||||
|   } | ||||
|  | ||||
|   .mapa-volver-btn { | ||||
|     top: 55px; | ||||
|     left: 12px; | ||||
|   } | ||||
|  | ||||
|   /* --- MEDIA QUERY ADICIONAL PARA MÓVIL EN HORIZONTAL --- */ | ||||
|   /* Se activa cuando la pantalla es ancha pero no muy alta, como un teléfono en landscape */ | ||||
|   @media (max-width: 900px) and (orientation: landscape) { | ||||
|  | ||||
|     /* Layout flexible de dos columnas */ | ||||
|     .panel-main-content { | ||||
|       display: flex; | ||||
|       flex-direction: row; | ||||
|       position: static; | ||||
|       height: 85vh; | ||||
|       min-height: 400px; | ||||
|     } | ||||
|  | ||||
|     .mapa-column, | ||||
|     .resultados-column { | ||||
|       position: static; | ||||
|       /* Desactivamos el posicionamiento absoluto */ | ||||
|       height: auto; | ||||
|       width: auto; | ||||
|       opacity: 1; | ||||
|       visibility: visible; | ||||
|       pointer-events: auto; | ||||
|       flex: 3; | ||||
|       overflow-y: auto; | ||||
|       /* Permitimos que la columna de resultados tenga su propio scroll */ | ||||
|     } | ||||
|  | ||||
|     .resultados-column { | ||||
|       flex: 2; | ||||
|       min-width: 300px; | ||||
|       /* Un mínimo para que no se comprima */ | ||||
|     } | ||||
|  | ||||
|     /* 3. Ocultamos los botones de cambio de vista móvil, ya que ambas se ven */ | ||||
|     .mobile-view-toggle { | ||||
|       display: none; | ||||
|     } | ||||
|  | ||||
|     /* 4. Mostramos de nuevo el botón lateral para colapsar el panel de resultados */ | ||||
|     .panel-toggle-btn { | ||||
|       display: flex; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -8,6 +8,8 @@ import { Breadcrumbs } from './components/Breadcrumbs'; | ||||
| import './PanelNacional.css'; | ||||
| import Select from 'react-select'; | ||||
| import type { PanelElectoralDto } from '../../../types/types'; | ||||
| import { FiMap, FiList } from 'react-icons/fi'; | ||||
| import { useMediaQuery } from './hooks/useMediaQuery'; | ||||
|  | ||||
| interface PanelNacionalWidgetProps { | ||||
|   eleccionId: number; | ||||
| @@ -38,6 +40,9 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|   const [ambitoActual, setAmbitoActual] = useState<AmbitoState>({ id: null, nivel: 'pais', nombre: 'Argentina', provinciaDistritoId: null }); | ||||
|   const [categoriaId, setCategoriaId] = useState<number>(2); | ||||
|   const [isPanelOpen, setIsPanelOpen] = useState(true); | ||||
|   const [mobileView, setMobileView] = useState<'mapa' | 'resultados'>('mapa'); | ||||
|   // --- DETECCIÓN DE VISTA MÓVIL --- | ||||
|   const isMobile = useMediaQuery('(max-width: 800px)'); | ||||
|  | ||||
|   const handleAmbitoSelect = (nuevoAmbitoId: string, nuevoNivel: 'provincia' | 'municipio', nuevoNombre: string) => { | ||||
|     setAmbitoActual(prev => ({ | ||||
| @@ -76,12 +81,14 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|     <div className="panel-nacional-container"> | ||||
|       <header className="panel-header"> | ||||
|         <div className="header-top-row"> | ||||
|           <h1>Resultados elecciones {ambitoActual.nombre}</h1> | ||||
|           <h1>Legislativas Argentina 2025</h1> | ||||
|           <Select | ||||
|             options={CATEGORIAS_NACIONALES} | ||||
|             value={selectedCategoria} | ||||
|             onChange={(option) => option && setCategoriaId(option.value)} | ||||
|             className="categoria-selector" | ||||
|             classNamePrefix="categoria-selector" | ||||
|             isSearchable={false} | ||||
|           /> | ||||
|         </div> | ||||
|         <Breadcrumbs | ||||
| @@ -92,7 +99,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|           onVolverProvincia={handleVolverAProvincia} | ||||
|         /> | ||||
|       </header> | ||||
|       <main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''}`}> | ||||
|       <main className={`panel-main-content ${!isPanelOpen ? 'panel-collapsed' : ''} ${isMobile ? `mobile-view-${mobileView}` : ''}`}> | ||||
|         <div className="mapa-column"> | ||||
|           <button className="panel-toggle-btn" onClick={() => setIsPanelOpen(!isPanelOpen)} title={isPanelOpen ? "Ocultar panel" : "Mostrar panel"}> | ||||
|             {isPanelOpen ? '›' : '‹'} | ||||
| @@ -107,6 +114,7 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|               provinciaDistritoId={ambitoActual.provinciaDistritoId ?? null} | ||||
|               onAmbitoSelect={handleAmbitoSelect} | ||||
|               onVolver={ambitoActual.nivel === 'municipio' ? handleVolverAProvincia : handleResetToPais} | ||||
|               isMobileView={isMobile} | ||||
|             /> | ||||
|           </Suspense> | ||||
|         </div> | ||||
| @@ -120,6 +128,24 @@ export const PanelNacionalWidget = ({ eleccionId }: PanelNacionalWidgetProps) => | ||||
|           </Suspense> | ||||
|         </div> | ||||
|       </main> | ||||
|  | ||||
|       {/* --- NUEVO CONTROLADOR DE VISTA PARA MÓVIL --- */} | ||||
|       <div className="mobile-view-toggle"> | ||||
|         <button | ||||
|           className={`toggle-btn ${mobileView === 'mapa' ? 'active' : ''}`} | ||||
|           onClick={() => setMobileView('mapa')} | ||||
|         > | ||||
|           <FiMap /> | ||||
|           <span>Mapa</span> | ||||
|         </button> | ||||
|         <button | ||||
|           className={`toggle-btn ${mobileView === 'resultados' ? 'active' : ''}`} | ||||
|           onClick={() => setMobileView('resultados')} | ||||
|         > | ||||
|           <FiList /> | ||||
|           <span>Resultados</span> | ||||
|         </button> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -9,6 +9,7 @@ import { API_BASE_URL, assetBaseUrl } from '../../../../apiService'; | ||||
| import type { ResultadoMapaDto, AmbitoGeography } from '../../../../types/types'; | ||||
| import { MapaProvincial } from './MapaProvincial'; | ||||
| import { CabaLupa } from './CabaLupa'; | ||||
| import { BiZoomIn, BiZoomOut } from "react-icons/bi"; | ||||
|  | ||||
| const DEFAULT_MAP_COLOR = '#E0E0E0'; | ||||
| const FADED_BACKGROUND_COLOR = '#F0F0F0'; | ||||
| @@ -17,7 +18,7 @@ const normalizarTexto = (texto: string = '') => texto.trim().toUpperCase().norma | ||||
| type PointTuple = [number, number]; | ||||
|  | ||||
| const PROVINCE_VIEW_CONFIG: Record<string, { center: PointTuple; zoom: number }> = { | ||||
|   "BUENOS AIRES": { center: [-60.5, -37.3], zoom: 5.5 }, | ||||
|   "BUENOS AIRES": { center: [-60.5, -37.3], zoom: 5 }, | ||||
|   "SANTA CRUZ": { center: [-69.5, -48.8], zoom: 5 }, | ||||
|   "CIUDAD AUTONOMA DE BUENOS AIRES": { center: [-58.45, -34.6], zoom: 85 }, | ||||
|   "CHUBUT": { center: [-68.5, -44.5], zoom: 5.5 }, | ||||
| @@ -31,7 +32,6 @@ const LUPA_SIZE_RATIO = 0.2; | ||||
| const MIN_LUPA_SIZE_PX = 100; | ||||
| const MAX_LUPA_SIZE_PX = 180; | ||||
|  | ||||
|  | ||||
| interface MapaNacionalProps { | ||||
|   eleccionId: number; | ||||
|   categoriaId: number; | ||||
| @@ -41,10 +41,19 @@ interface MapaNacionalProps { | ||||
|   provinciaDistritoId: string | null; | ||||
|   onAmbitoSelect: (ambitoId: string, nivel: 'provincia' | 'municipio', nombre: string) => void; | ||||
|   onVolver: () => void; | ||||
|   isMobileView: boolean; | ||||
| } | ||||
|  | ||||
| export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver }: MapaNacionalProps) => { | ||||
|   const [position, setPosition] = useState({ zoom: 1, center: [-65, -40] as PointTuple }); | ||||
| // --- CONFIGURACIONES DEL MAPA --- | ||||
| const desktopProjectionConfig = { scale: 700, center: [-65, -40] as [number, number] }; | ||||
| const mobileProjectionConfig = { scale: 1100, center: [-64, -41] as [number, number] }; | ||||
|  | ||||
| export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nombreProvinciaActiva, provinciaDistritoId, onAmbitoSelect, onVolver, isMobileView }: MapaNacionalProps) => { | ||||
|   const [position, setPosition] = useState({ | ||||
|     zoom: isMobileView ? 1.5 : 1.05, // 1.5 para móvil, 1.05 para desktop | ||||
|     center: [-65, -40] as PointTuple | ||||
|   }); | ||||
|   const initialProvincePositionRef = useRef<{ zoom: number, center: PointTuple } | null>(null); | ||||
|  | ||||
|   const containerRef = useRef<HTMLDivElement | null>(null); | ||||
|   const lupaRef = useRef<HTMLDivElement | null>(null); | ||||
| @@ -70,21 +79,33 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|  | ||||
|   useEffect(() => { | ||||
|     if (nivel === 'pais') { | ||||
|       setPosition({ zoom: 1, center: [-65, -40] }); | ||||
|       setPosition({ | ||||
|         zoom: isMobileView ? 1.4 : 1.05, // 1.5 para móvil, 1.05 para desktop | ||||
|         center: [-65, -40] | ||||
|       }); | ||||
|       // Reseteamos el ref | ||||
|       initialProvincePositionRef.current = null; | ||||
|     } else if (nivel === 'provincia') { | ||||
|       const nombreNormalizado = normalizarTexto(nombreAmbito); | ||||
|       const manualConfig = PROVINCE_VIEW_CONFIG[nombreNormalizado]; | ||||
|  | ||||
|       let provinceConfig = { zoom: 7, center: [-65, -40] as PointTuple }; | ||||
|  | ||||
|       if (manualConfig) { | ||||
|         setPosition(manualConfig); | ||||
|         provinceConfig = manualConfig; | ||||
|       } else { | ||||
|         const provinciaGeo = geoDataNacional.objects.provincias.geometries.find((g: any) => normalizarTexto(g.properties.nombre) === nombreNormalizado); | ||||
|         if (provinciaGeo) { | ||||
|           const provinciaFeature = feature(geoDataNacional, provinciaGeo); | ||||
|           const centroid = geoCentroid(provinciaFeature); | ||||
|           setPosition({ zoom: 7, center: centroid as PointTuple }); | ||||
|           provinceConfig = { zoom: 7, center: centroid as PointTuple }; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       setPosition(provinceConfig); | ||||
|  | ||||
|       // --- Guardar el objeto de posición completo en el ref --- | ||||
|       initialProvincePositionRef.current = provinceConfig; | ||||
|     } | ||||
|   }, [nivel, nombreAmbito, geoDataNacional]); | ||||
|  | ||||
| @@ -104,7 +125,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|  | ||||
|         const calculatedSize = containerRect.width * LUPA_SIZE_RATIO; | ||||
|         const newLupaSize = Math.max(MIN_LUPA_SIZE_PX, Math.min(calculatedSize, MAX_LUPA_SIZE_PX)); | ||||
|          | ||||
|  | ||||
|         const horizontalOffset = newLupaSize * 0.5; | ||||
|         const verticalOffset = newLupaSize * 0.2; | ||||
|  | ||||
| @@ -132,7 +153,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|     if (containerRef.current) { | ||||
|       resizeObserver.observe(containerRef.current); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     let timerId: NodeJS.Timeout; | ||||
|  | ||||
|     if (initialLoadRef.current && nivel === 'pais') { | ||||
| @@ -159,20 +180,93 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|     }; | ||||
|   }, [position, nivel]); | ||||
|  | ||||
|   // --- HANDLERS PARA EL ZOOM --- | ||||
|   const handleZoomIn = () => { | ||||
|     setPosition(prev => ({ | ||||
|       ...prev, | ||||
|       zoom: Math.min(prev.zoom * 1.8, 100) // Multiplica el zoom actual, con un límite | ||||
|     })); | ||||
|   }; | ||||
|  | ||||
|   // --- Lógica de reseteo en handleZoomOut --- | ||||
|   const handleZoomOut = () => { | ||||
|     setPosition(prev => { | ||||
|       const newZoom = Math.max(prev.zoom / 1.8, 1); | ||||
|       const initialPos = initialProvincePositionRef.current; | ||||
|  | ||||
|       // Si estamos en una provincia Y el nuevo zoom es igual o menor que el inicial... | ||||
|       if (initialPos && newZoom <= initialPos.zoom) { | ||||
|         // ...reseteamos a la posición inicial guardada (zoom Y centro). | ||||
|         return initialPos; | ||||
|       } | ||||
|  | ||||
|       // Si no, solo actualizamos el zoom. | ||||
|       return { ...prev, zoom: newZoom }; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleMoveEnd = (newPosition: { coordinates: PointTuple, zoom: number }) => { | ||||
|     // Solo actualizamos el centro (coordenadas), no el zoom, al arrastrar | ||||
|     setPosition(prev => ({ ...prev, center: newPosition.coordinates })); | ||||
|   }; | ||||
|  | ||||
|   const panEnabled = | ||||
|     //isMobileView && | ||||
|     nivel === 'provincia' && | ||||
|     initialProvincePositionRef.current !== null && | ||||
|     position.zoom > initialProvincePositionRef.current.zoom && | ||||
|     !nombreMunicipioSeleccionado; | ||||
|  | ||||
|   const showZoomControls = nivel === 'provincia'; | ||||
|  | ||||
|   // --- FUNCIÓN DE FILTRO --- | ||||
|   const filterInteractionEvents = (event: any) => { | ||||
|     // La librería pasa un objeto de evento que contiene el evento original del navegador. | ||||
|     // Si el evento original es de la rueda del ratón ('wheel'), siempre lo bloqueamos. | ||||
|     if (event.sourceEvent && event.sourceEvent.type === 'wheel') { | ||||
|       return false; | ||||
|     } | ||||
|     // Para cualquier otro evento (arrastre, etc.), la decisión depende de nuestra lógica `panEnabled`. | ||||
|     return panEnabled; | ||||
|   }; | ||||
|  | ||||
|   // --- LÓGICA PARA DESHABILITAR EL BOTÓN --- | ||||
|   const isZoomOutDisabled = | ||||
|     (nivel === 'provincia' && initialProvincePositionRef.current && position.zoom <= initialProvincePositionRef.current.zoom) || | ||||
|     (nivel === 'pais' && position.zoom <= (isMobileView ? 1.4 : 1.05)); | ||||
|  | ||||
|   return ( | ||||
|     <div className="mapa-componente-container" ref={containerRef}> | ||||
|       {showZoomControls && ( | ||||
|         <div className="zoom-controls-container"> | ||||
|           <button onClick={handleZoomIn} className="zoom-btn" title="Acercar"> | ||||
|             <span className="zoom-icon-wrapper"><BiZoomIn /></span> | ||||
|           </button> | ||||
|            | ||||
|           <button | ||||
|             onClick={handleZoomOut} | ||||
|             className={`zoom-btn ${isZoomOutDisabled ? 'disabled' : ''}`} | ||||
|             title="Alejar" | ||||
|             disabled={isZoomOutDisabled} | ||||
|           > | ||||
|             <span className="zoom-icon-wrapper"><BiZoomOut /></span> | ||||
|           </button> | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       {nivel !== 'pais' && <button onClick={onVolver} className="mapa-volver-btn">← Volver</button>} | ||||
|  | ||||
|       <div className="mapa-render-area"> | ||||
|         <ComposableMap | ||||
|           projection="geoMercator" | ||||
|           projectionConfig={{ scale: 700, center: [-65, -40] }} | ||||
|           projectionConfig={isMobileView ? mobileProjectionConfig : desktopProjectionConfig} | ||||
|           style={{ width: "100%", height: "100%" }} | ||||
|         > | ||||
|           <ZoomableGroup | ||||
|             center={position.center} | ||||
|             zoom={position.zoom} | ||||
|             filterZoomEvent={() => false} | ||||
|             onMoveEnd={handleMoveEnd} | ||||
|             filterZoomEvent={filterInteractionEvents} | ||||
|           > | ||||
|             <Geographies geography={geoDataNacional}> | ||||
|               {({ geographies }: { geographies: AmbitoGeography[] }) => geographies.map((geo) => { | ||||
| @@ -233,7 +327,7 @@ export const MapaNacional = ({ eleccionId, categoriaId, nivel, nombreAmbito, nom | ||||
|         </div> | ||||
|       )} | ||||
|  | ||||
|       <Tooltip id="mapa-tooltip" /> | ||||
|       <Tooltip id="mapa-tooltip" key={nivel} /> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @@ -0,0 +1,31 @@ | ||||
| // src/hooks/useMediaQuery.ts | ||||
| import { useState, useEffect } from 'react'; | ||||
|  | ||||
| export const useMediaQuery = (query: string): boolean => { | ||||
|   const [matches, setMatches] = useState(false); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const media = window.matchMedia(query); | ||||
|     if (media.matches !== matches) { | ||||
|       setMatches(media.matches); | ||||
|     } | ||||
|      | ||||
|     const listener = () => setMatches(media.matches); | ||||
|     // Deprecated 'addListener' for broader browser support, 'addEventListener' is preferred. | ||||
|     if (media.addEventListener) { | ||||
|       media.addEventListener('change', listener); | ||||
|     } else { | ||||
|       media.addListener(listener); | ||||
|     } | ||||
|  | ||||
|     return () => { | ||||
|       if (media.removeEventListener) { | ||||
|         media.removeEventListener('change', listener); | ||||
|       } else { | ||||
|         media.removeListener(listener); | ||||
|       } | ||||
|     }; | ||||
|   }, [matches, query]); | ||||
|  | ||||
|   return matches; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user