ViewModeSelector.svelte
1 <script lang="ts"> 2 import { createEventDispatcher } from 'svelte'; 3 import { viewColumns, viewPreferences } from '$lib/stores'; 4 import { get } from 'svelte/store'; 5 import { onMount } from 'svelte'; 6 import { page } from '$app/stores'; 7 import { browser } from '$app/environment'; 8 9 // Necesario para que TypeScript reconozca $page correctamente 10 export {}; 11 12 const dispatch = createEventDispatcher(); 13 14 // Prop para el tipo de búsqueda (opcional) 15 export let searchType: string = ''; 16 17 // Extraer URL para uso reactivo 18 $: pageUrl = $page.url; 19 $: typeParam = pageUrl.searchParams.get('type'); 20 21 // Obtener el tipo de búsqueda desde la URL si no se proporciona 22 $: actualSearchType = searchType || typeParam || 'web'; 23 24 // Obtener el valor actual de columnas para este tipo de búsqueda 25 $: columns = getColumnsForCurrentType(); 26 27 function getColumnsForCurrentType(): number { 28 // Obtener valor de localStorage si existe 29 if (typeof window !== 'undefined') { 30 const storedPref = localStorage.getItem(`viewColumns_${actualSearchType}`); 31 if (storedPref) { 32 const value = parseInt(storedPref, 10); 33 if (!isNaN(value)) return value; 34 } 35 } 36 37 // Obtener preferencias para el tipo actual 38 const preferences = get(viewPreferences); 39 if (preferences && preferences[actualSearchType]) { 40 return preferences[actualSearchType].columns; 41 } 42 43 // Si no hay preferencia específica, usar el valor global 44 return get(viewColumns); 45 } 46 47 // Variable para almacenar el tamaño de la pantalla 48 let windowWidth = typeof window !== 'undefined' ? window.innerWidth : 1024; 49 50 // Mantener actualizada la lista de opciones disponibles según el tamaño de pantalla 51 $: availableColumns = getAvailableColumns(windowWidth); 52 53 // Determinar las opciones de columnas disponibles según el ancho 54 function getAvailableColumns(width: number) { 55 // Para dispositivos móviles solo mostramos vista de lista y 2 columnas 56 if (width < 480) { 57 return [ 58 { value: 1, label: 'Lista', icon: 'list' }, 59 { value: 2, label: '2 columnas', icon: 'grid-2' } 60 ]; 61 } 62 63 // Para tablets mostramos hasta 3 columnas 64 if (width < 768) { 65 return [ 66 { value: 1, label: 'Lista', icon: 'list' }, 67 { value: 2, label: '2 columnas', icon: 'grid-2' }, 68 { value: 3, label: '3 columnas', icon: 'grid-3' } 69 ]; 70 } 71 72 // Para laptops mostramos hasta 4 columnas 73 if (width < 1200) { 74 return [ 75 { value: 1, label: 'Lista', icon: 'list' }, 76 { value: 2, label: '2 columnas', icon: 'grid-2' }, 77 { value: 3, label: '3 columnas', icon: 'grid-3' }, 78 { value: 4, label: '4 columnas', icon: 'grid-4' } 79 ]; 80 } 81 82 // Para pantallas grandes mostramos todas las opciones 83 return [ 84 { value: 1, label: 'Lista', icon: 'list' }, 85 { value: 2, label: '2 columnas', icon: 'grid-2' }, 86 { value: 3, label: '3 columnas', icon: 'grid-3' }, 87 { value: 4, label: '4 columnas', icon: 'grid-4' }, 88 { value: 6, label: '6 columnas', icon: 'grid-6' } 89 ]; 90 } 91 92 // Actualizar cuando cambia el tamaño de la ventana 93 function handleResize() { 94 windowWidth = typeof window !== 'undefined' ? window.innerWidth : 1024; 95 96 // Si la selección actual no está disponible en el nuevo tamaño, ajustar a un valor válido 97 if (!availableColumns.some(option => option.value === columns)) { 98 // Encontrar el valor máximo disponible que sea menor o igual que el valor actual 99 const maxAvailable = Math.max(...availableColumns.map(option => option.value)); 100 updateColumns(Math.min(columns, maxAvailable)); 101 } 102 } 103 104 // Configurar event listener para resize 105 onMount(() => { 106 if (typeof window !== 'undefined') { 107 window.addEventListener('resize', handleResize); 108 } 109 return () => { 110 if (typeof window !== 'undefined') { 111 window.removeEventListener('resize', handleResize); 112 } 113 }; 114 }); 115 116 function updateColumns(newValue: number) { 117 columns = newValue; 118 119 // Guardar para el tipo de búsqueda actual 120 viewPreferences.update(prefs => { 121 const updatedPrefs = { ...prefs }; 122 if (!updatedPrefs[actualSearchType]) { 123 updatedPrefs[actualSearchType] = {}; 124 } 125 updatedPrefs[actualSearchType].columns = newValue; 126 return updatedPrefs; 127 }); 128 129 // También actualizar el store global para compatibilidad 130 viewColumns.set(columns); 131 132 // Guardar en localStorage para persistencia 133 if (typeof window !== 'undefined') { 134 localStorage.setItem(`viewColumns_${actualSearchType}`, newValue.toString()); 135 } 136 137 // Notificar cambio 138 dispatch('change', { columns, searchType: actualSearchType }); 139 } 140 141 // Iconos para cada tipo de vista 142 const icons = { 143 list: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg>`, 144 'grid-2': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9"></rect><rect x="14" y="3" width="7" height="9"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect></svg>`, 145 'grid-3': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="5" height="9"></rect><rect x="10" y="3" width="5" height="9"></rect><rect x="17" y="3" width="4" height="9"></rect><rect x="3" y="14" width="5" height="7"></rect><rect x="10" y="14" width="5" height="7"></rect><rect x="17" y="14" width="4" height="7"></rect></svg>`, 146 'grid-4': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="4" height="7"></rect><rect x="9" y="3" width="4" height="7"></rect><rect x="15" y="3" width="4" height="7"></rect><rect x="21" y="3" width="0" height="7"></rect><rect x="3" y="12" width="4" height="7"></rect><rect x="9" y="12" width="4" height="7"></rect><rect x="15" y="12" width="4" height="7"></rect><rect x="21" y="12" width="0" height="7"></rect></svg>`, 147 'grid-6': `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="3" height="7"></rect><rect x="8" y="3" width="3" height="7"></rect><rect x="13" y="3" width="3" height="7"></rect><rect x="18" y="3" width="3" height="7"></rect><rect x="3" y="12" width="3" height="7"></rect><rect x="8" y="12" width="3" height="7"></rect><rect x="13" y="12" width="3" height="7"></rect><rect x="18" y="12" width="3" height="7"></rect></svg>`, 148 }; 149 </script> 150 151 <div class="view-mode-selector"> 152 <div class="view-mode-label"> 153 <span>Vista:</span> 154 </div> 155 156 <div class="view-mode-buttons"> 157 {#each availableColumns as option} 158 <button 159 class="view-mode-button {columns === option.value ? 'active' : ''}" 160 on:click={() => updateColumns(option.value)} 161 aria-label="Cambiar a vista {option.label}" 162 title="{option.label}" 163 > 164 {@html icons[option.icon]} 165 </button> 166 {/each} 167 </div> 168 </div> 169 170 <style> 171 .view-mode-selector { 172 display: flex; 173 align-items: center; 174 gap: var(--space-sm); 175 color: var(--text-secondary); 176 padding: var(--space-sm) var(--space-md); 177 background: var(--bg-card-primary); 178 border: 1px solid var(--primary); 179 border-radius: var(--border-radius); 180 transition: all 0.3s ease; 181 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); 182 } 183 184 /* Ajustes para tema oscuro */ 185 :global(body.dark) .view-mode-selector { 186 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 187 } 188 189 /* Añadir efecto de hover para hacer más visible el selector */ 190 .view-mode-selector:hover { 191 transform: translateY(-2px); 192 box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.3); 193 } 194 195 .view-mode-label { 196 font-size: 0.9rem; 197 font-weight: var(--font-weight-medium); 198 white-space: nowrap; 199 } 200 201 .view-mode-buttons { 202 display: flex; 203 align-items: center; 204 gap: var(--space-xs); 205 } 206 207 .view-mode-button { 208 background: transparent; 209 border: 1px solid transparent; 210 color: var(--text-secondary); 211 width: 38px; 212 height: 38px; 213 padding: 0; 214 border-radius: var(--border-radius-sm); 215 display: flex; 216 align-items: center; 217 justify-content: center; 218 cursor: pointer; 219 transition: all var(--transition-speed); 220 position: relative; 221 overflow: hidden; 222 } 223 224 .view-mode-button:hover { 225 background-color: var(--bg-secondary); 226 color: var(--primary); 227 border-color: var(--border-color); 228 transform: scale(1.05); 229 } 230 231 .view-mode-button.active { 232 background-color: var(--primary); 233 color: white; 234 border-color: var(--primary); 235 box-shadow: 0 2px 4px rgba(var(--primary-rgb), 0.3); 236 } 237 238 .view-mode-button.active:hover { 239 transform: scale(1.05); 240 box-shadow: 0 2px 8px rgba(var(--primary-rgb), 0.5); 241 } 242 243 /* Añadir efecto al hacer clic */ 244 .view-mode-button:active { 245 transform: scale(0.95); 246 } 247 248 @media (max-width: 768px) { 249 .view-mode-selector { 250 padding: var(--space-xs) var(--space-sm); 251 } 252 253 .view-mode-button { 254 width: 30px; 255 height: 30px; 256 } 257 } 258 259 @media (max-width: 480px) { 260 .view-mode-selector { 261 width: 100%; 262 justify-content: space-between; 263 } 264 } 265 </style>