+page.svelte
1 <script lang="ts"> 2 import ResultsList from './components/ResultsList.svelte'; 3 import type { SearchResult } from '$lib/types'; 4 import { onMount } from 'svelte'; 5 import { searchParams } from '$lib/stores'; 6 import { page } from '$app/stores'; 7 import { get } from 'svelte/store'; 8 import { browser } from '$app/environment'; 9 10 // Necesario para que TypeScript reconozca $page correctamente 11 export {}; 12 13 let results: SearchResult[] = []; 14 let query = ''; 15 let type: 'web' | 'images' | 'document' | 'app' = 'web'; 16 let language = 'all'; 17 let page_num = 0; 18 let isSearching = false; 19 let searchError = ''; 20 let totalResults = 0; 21 let searchTime = 0; 22 23 // Variables reactivas 24 let pageUrl = new URL('http://localhost'); 25 let hasQuery = false; 26 let isHomePage = true; 27 28 let activeFilters = { 29 resultadosPorPagina: 10, 30 recurso: 'local', // Valores aceptados: 'local' o 'peer' (se mapea a 'global' en el servidor) 31 preferMask: '', 32 constraints: 'false', // Sin restricciones para resolver problemas de paginación 33 mediaSearch: 'extended_strict' 34 }; 35 36 // Determinar si estamos en la home 37 $: { 38 if ($page) { 39 pageUrl = $page.url; 40 hasQuery = pageUrl.searchParams.has('query'); 41 isHomePage = !hasQuery; 42 } 43 } 44 45 let pageQuery = ''; 46 let pageType: 'web' | 'images' | 'document' | 'app' = 'web'; 47 let pageLanguage = 'all'; 48 49 // Actualización reactiva de los params de URL 50 $: if (pageUrl) { 51 pageQuery = pageUrl.searchParams.get('query') ?? ''; 52 pageType = (pageUrl.searchParams.get('type') as 'web' | 'images' | 'document' | 'app') ?? 'web'; 53 pageLanguage = pageUrl.searchParams.get('language') ?? 'all'; 54 } 55 56 // Variable para evitar bucles infinitos 57 let isInitialLoad = true; 58 59 // Reaccionar a cambios de la URL 60 $: if (hasQuery) { 61 if ((query !== pageQuery || type !== pageType || language !== pageLanguage) && !isSearching) { 62 console.log('URL actualizada, nueva búsqueda:', { pageQuery, pageType, pageLanguage }); 63 query = pageQuery; 64 type = pageType; 65 language = pageLanguage; 66 page_num = 0; 67 results = []; 68 69 // Solo hacer la búsqueda si no es la carga inicial o si hay un query válido 70 if (!isInitialLoad || (query && query.trim())) { 71 console.log('Ejecutando búsqueda desde cambio de URL'); 72 fetchResults(); 73 } 74 75 // Después de la primera ejecución, ya no es carga inicial 76 isInitialLoad = false; 77 } 78 } 79 80 async function fetchResults() { 81 try { 82 console.log('Iniciando búsqueda...'); 83 isSearching = true; 84 searchError = ''; 85 86 // Construir parámetros de forma explícita siguiendo la documentación 87 const params = new URLSearchParams(); 88 89 // Asegurarse que query no esté vacío 90 if (!query || !query.trim()) { 91 searchError = 'Por favor ingresa un término de búsqueda'; 92 isSearching = false; 93 return; 94 } 95 96 // Parámetros obligatorios 97 params.set('query', query.trim()); 98 params.set('type', type); 99 params.set('language', language || 'all'); 100 params.set('page', page_num.toString()); 101 102 // Parámetros opcionales de filtros avanzados 103 params.set('resultadosPorPagina', activeFilters.resultadosPorPagina.toString()); 104 params.set('recurso', activeFilters.recurso || 'global'); // Usar global como valor por defecto para debugging 105 106 // Solo añadir estos parámetros si tienen valor 107 if (activeFilters.preferMask) { 108 params.set('preferMask', activeFilters.preferMask); 109 } 110 111 if (activeFilters.constraints) { 112 params.set('constraints', activeFilters.constraints); 113 } 114 115 if (activeFilters.mediaSearch) { 116 params.set('mediaSearch', activeFilters.mediaSearch); 117 } 118 119 console.log('Parámetros de búsqueda:', Object.fromEntries(params.entries())); 120 121 // Construir la URL completa para debugging 122 console.log('URL completa para API proxy:', `/api/search?${params.toString()}`); 123 124 // Usar un timeout más largo para debugging 125 const controller = new AbortController(); 126 const timeout = setTimeout(() => controller.abort(), 20000); 127 128 let res; 129 try { 130 // Intentar primero con nuestra API 131 console.log('Fetching desde API proxy:', `/api/search?${params.toString()}`); 132 res = await fetch(`/api/search?${params.toString()}`, { 133 signal: controller.signal, 134 headers: { 135 'Accept': 'application/json', 136 'Cache-Control': 'no-cache' 137 } 138 }); 139 clearTimeout(timeout); 140 141 if (!res.ok) { 142 console.error(`Error HTTP: ${res.status}: ${res.statusText}`); 143 throw new Error(`Error ${res.status}: ${res.statusText}`); 144 } 145 } catch (fetchError) { 146 // Capturar errores de red aquí 147 console.error('Error de red:', fetchError); 148 searchError = fetchError.message || 'Error de red al conectar con el servidor. Verifique que YaCy esté en ejecución.'; 149 isSearching = false; 150 return; 151 } 152 153 // Si llegamos aquí, tenemos respuesta HTTP 154 let data; 155 try { 156 // Obtener texto para debug 157 const responseText = await res.text(); 158 console.log('Respuesta cruda:', responseText.substring(0, 500) + '...'); 159 160 try { 161 // Intentar parsear JSON 162 data = JSON.parse(responseText); 163 console.log('Datos JSON recibidos:', data); 164 } catch (parseError) { 165 console.error('Error al parsear JSON:', parseError); 166 searchError = 'La respuesta no es un JSON válido'; 167 isSearching = false; 168 return; 169 } 170 } catch (textError) { 171 console.error('Error al obtener texto de respuesta:', textError); 172 searchError = 'Error al leer la respuesta del servidor'; 173 isSearching = false; 174 return; 175 } 176 177 // Manejar errores en la respuesta 178 if (data.error || data.pagination_error) { 179 // Si hay un error de paginación específico de YaCy 180 if (data.pagination_error) { 181 console.warn('Error de paginación YaCy:', data.error_message); 182 searchError = data.error_message || 'Error en la paginación de resultados'; 183 184 // No intentamos más paginación en este caso 185 page_num--; // Retroceder a la página anterior 186 187 // Finalizar búsqueda pero mantener resultados actuales 188 isSearching = false; 189 return; 190 } 191 // Otros errores generales 192 else if (data.error) { 193 console.warn('Error en la respuesta:', data.error); 194 195 // Personalizar mensaje de error según el contexto 196 if (data.error.includes('YaCy') || data.error.toLowerCase().includes('timeout')) { 197 // Error de conexión con YaCy 198 searchError = 'No se pudo conectar con el servidor YaCy. Verifica que esté funcionando.'; 199 } else if (data.error.includes('formato')) { 200 // Error de formato en la respuesta 201 searchError = 'La respuesta del servidor no tiene el formato esperado.'; 202 } else { 203 // Otro tipo de error 204 searchError = data.error || 'Error desconocido en la búsqueda'; 205 } 206 207 // Si no hay resultados, terminar 208 if (!data.results || !Array.isArray(data.results) || data.results.length === 0) { 209 console.log('No hay resultados debido a un error:', searchError); 210 isSearching = false; 211 results = []; 212 return; 213 } 214 215 // Si hay resultados a pesar del error, continuar pero mostrar el error 216 console.log('Hay resultados a pesar del error, continuando...'); 217 } 218 } 219 220 // Verificar si la respuesta tiene la estructura esperada 221 if (!data.results) { 222 console.warn('Estructura de respuesta inesperada:', data); 223 224 // Intentar extraer resultados de la estructura canónica de YaCy si existe 225 if (data.channels && Array.isArray(data.channels) && data.channels.length > 0 && 226 data.channels[0].items && Array.isArray(data.channels[0].items)) { 227 console.log('Adaptando estructura de canales de YaCy'); 228 229 try { 230 // Adaptar la estructura de YaCy a nuestro formato 231 const channel = data.channels[0]; 232 233 // Validar cada campo necesario 234 if (!channel.items || !Array.isArray(channel.items)) { 235 throw new Error('Canal sin items válidos'); 236 } 237 238 // Crear estructura compatible 239 data = { 240 results: channel.items.map((item: any) => { 241 // Validar que el item tenga link 242 if (!item.link) { 243 console.warn('Item sin link:', item); 244 item.link = '#'; 245 } 246 247 // Crear objeto compatible 248 return { 249 title: item.title || 'Sin título', 250 link: item.link, 251 description: item.description || item.snippet || '', 252 type: type, 253 domain: item.link !== '#' ? new URL(item.link).hostname.replace(/^www\./, '') : '', 254 formattedUrl: item.link 255 }; 256 }), 257 totalResults: parseInt(channel.totalResults || '0', 10), 258 searchTime: 0.5, 259 query: query, 260 page: page_num 261 }; 262 } catch (adaptError) { 263 console.error('Error adaptando estructura:', adaptError); 264 searchError = 'Error al procesar la respuesta de YaCy'; 265 isSearching = false; 266 results = []; 267 return; 268 } 269 } else { 270 // No se pudo adaptar, devolver error 271 searchError = 'La respuesta no tiene el formato esperado'; 272 isSearching = false; 273 results = []; 274 return; 275 } 276 } 277 278 if (!Array.isArray(data.results)) { 279 console.error('data.results no es un array:', data.results); 280 searchError = 'El formato de respuesta es inválido'; 281 isSearching = false; 282 return; 283 } 284 285 console.log(`Procesando ${data.results.length} resultados`); 286 287 // Procesar resultados 288 if (data.results.length === 0) { 289 console.log(`No hay resultados disponibles para la página ${page_num}`); 290 291 // Si es la primera página, reemplazar completamente los resultados 292 if (page_num === 0) { 293 results = []; 294 searchError = 'No se encontraron resultados para tu búsqueda'; 295 } 296 // Si no es la primera página, mantener resultados anteriores 297 else { 298 searchError = 'No hay más resultados disponibles'; 299 } 300 } else { 301 // Páginas adicionales - añadir manteniendo unicidad 302 const existingLinks = new Set(results.map(r => r.link)); 303 const uniqueNewResults = data.results.filter(r => !existingLinks.has(r.link)); 304 305 // Añadir solo si hay nuevos resultados 306 if (uniqueNewResults.length > 0) { 307 results = [...results, ...uniqueNewResults]; 308 } else if (data.results.length > 0) { 309 console.log('Se encontraron resultados pero todos ya existían en la lista'); 310 } 311 } 312 313 searchTime = data.searchTime ?? 0; 314 totalResults = data.totalResults ?? 0; 315 316 // Éxito - finalizar la búsqueda 317 isSearching = false; 318 console.log('Búsqueda completada con éxito:', { 319 resultCount: results.length, 320 totalResults, 321 searchTime 322 }); 323 324 } catch (error) { 325 console.error('Error en búsqueda:', error); 326 const errorMessage = error instanceof Error ? error.message : 'Error desconocido'; 327 328 if (error instanceof Error && error.name === 'AbortError') { 329 searchError = 'La búsqueda tardó demasiado tiempo. Verifica que YaCy esté funcionando en http://localhost:8090.'; 330 } else if (errorMessage.includes('YaCy')) { 331 searchError = `Error al conectar con YaCy. ${errorMessage}`; 332 } else { 333 searchError = errorMessage; 334 } 335 336 if (page_num === 0) results = []; 337 338 // Error - finalizar la búsqueda 339 isSearching = false; 340 } finally { 341 // Asegurarse de que isSearching siempre se resetea 342 setTimeout(() => { 343 if (isSearching) { 344 console.warn('isSearching no se reseteo correctamente, forzando reset'); 345 isSearching = false; 346 } 347 }, 5000); 348 } 349 } 350 351 async function loadMore() { 352 if (isSearching || !query.trim()) return; 353 354 // Evitar cargar más páginas si ya hemos llegado al límite de resultados 355 // Asumiendo que cada página muestra activeFilters.resultadosPorPagina resultados 356 const maxPages = totalResults ? Math.ceil(totalResults / activeFilters.resultadosPorPagina) : 100; 357 358 if (page_num >= maxPages - 1) { 359 console.log(`Ya estamos en la última página (${page_num+1} de ${maxPages}) según totalResults=${totalResults}`); 360 searchError = 'Has alcanzado el final de los resultados disponibles'; 361 return; 362 } 363 364 const prevScrollPos = window.scrollY; 365 const prevResultsCount = results.length; 366 page_num++; 367 368 await fetchResults(); 369 370 const newResultsCount = results.length - prevResultsCount; 371 372 if (type === 'images') { 373 showScrollIndicator(newResultsCount); 374 window.scrollTo({ top: prevScrollPos, behavior: 'auto' }); 375 } else { 376 const newResults = document.querySelectorAll(`.result-card:nth-child(n+${prevResultsCount + 1})`); 377 if (newResults.length > 0) { 378 const offset = 100; 379 const topPos = (newResults[0] as HTMLElement).offsetTop - offset; 380 window.scrollTo({ top: topPos, behavior: 'smooth' }); 381 } 382 } 383 } 384 385 function showScrollIndicator(count: number) { 386 const indicator = document.createElement('div'); 387 indicator.textContent = `Cargados ${count} resultados nuevos`; 388 indicator.style.cssText = ` 389 position: fixed; 390 bottom: 20px; 391 left: 50%; 392 transform: translateX(-50%); 393 background: var(--primary); 394 color: white; 395 padding: 8px 16px; 396 border-radius: 20px; 397 z-index: 1001; 398 font-size: 14px; 399 box-shadow: 0 2px 8px rgba(0,0,0,0.2); 400 opacity: 0; 401 transition: opacity 0.3s ease; 402 `; 403 document.body.appendChild(indicator); 404 setTimeout(() => { 405 indicator.style.opacity = '1'; 406 setTimeout(() => { 407 indicator.style.opacity = '0'; 408 setTimeout(() => indicator.remove(), 300); 409 }, 3000); 410 }, 100); 411 } 412 413 onMount(() => { 414 const urlParams = new URLSearchParams(window.location.search); 415 const urlQuery = urlParams.get('query'); 416 417 if (urlQuery) { 418 query = urlQuery; 419 type = (urlParams.get('type') as 'web' | 'images' | 'document' | 'app') ?? 'web'; 420 language = urlParams.get('language') ?? 'all'; 421 page_num = 0; 422 results = []; 423 fetchResults(); 424 } else { 425 fetch('/api/yacy/search') 426 .then(res => res.ok ? console.log('YaCy está listo') : console.warn('YaCy no responde')) 427 .catch(() => console.warn('No se pudo verificar YaCy')); 428 } 429 }); 430 431 searchParams.subscribe(params => { 432 if (params.query) { 433 query = params.query; 434 type = params.type as 'web' | 'images' | 'document' | 'app'; 435 language = params.language; 436 page_num = 0; 437 results = []; 438 fetchResults(); 439 } 440 }); 441 </script> 442 443 444 445 <!-- Contenedor principal con clases condicionales basadas en si es página de inicio o resultados --> 446 <div class="main-container {isHomePage ? 'home-view' : 'results-view'}"> 447 <!-- Eliminado el contenido duplicado del texto de la página principal --> 448 <!-- Ahora solo se muestra el contenido desde +layout.svelte --> 449 450 <!-- Eliminados controles de vista --> 451 452 <!-- Página de resultados de búsqueda --> 453 {#if query && !isHomePage} 454 <section class="results-section"> 455 <!-- Cabecera de resultados --> 456 <div class="results-header"> 457 {#if isSearching && page_num === 0} 458 <div class="results-status"> 459 <div class="loader"></div> 460 <p>Buscando resultados para "{query}"...</p> 461 </div> 462 {:else if searchError} 463 <div class="results-error"> 464 <div class="error-icon"> 465 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 466 <circle cx="12" cy="12" r="10"></circle> 467 <line x1="12" y1="8" x2="12" y2="12"></line> 468 <line x1="12" y1="16" x2="12.01" y2="16"></line> 469 </svg> 470 </div> 471 <div class="error-content"> 472 <h3>Error al realizar la búsqueda</h3> 473 <p>{searchError}</p> 474 <div class="error-help"> 475 <a href="http://127.0.0.1:8090" target="_blank" rel="noopener"> 476 Verificar estado de YaCy → 477 </a> 478 </div> 479 <div class="error-actions"> 480 <button class="retry-button" on:click={() => fetchResults()}> 481 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 482 <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/> 483 </svg> 484 <span>Reintentar</span> 485 </button> 486 <!-- Sin modo debug --> 487 </div> 488 </div> 489 </div> 490 {:else if results.length > 0} 491 <div class="results-info"> 492 <p>Aproximadamente <strong>{totalResults.toLocaleString()}</strong> resultados <span class="search-time">({searchTime.toFixed(2)} segundos)</span></p> 493 </div> 494 {/if} 495 </div> 496 497 <!-- Mostrar componente adecuado según el tipo de búsqueda --> 498 {#if type === 'maps'} 499 <div class="map-view-container"> 500 <MapResults 501 isMapMode={true} 502 searchQuery={query} 503 /> 504 </div> 505 {:else} 506 <ResultsList 507 {results} 508 {loadMore} 509 searchType={type} 510 loading={isSearching && page_num > 0} 511 /> 512 {/if} 513 514 <!-- Sin panel de depuración --> 515 </section> 516 {:else if searchError && searchError.includes('YaCy')} 517 <!-- Mostrar cuando hay un error con YaCy --> 518 <div class="yacy-error-container"> 519 <div class="yacy-error-icon"> 520 <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"> 521 <circle cx="12" cy="12" r="10"></circle> 522 <line x1="12" y1="8" x2="12" y2="12"></line> 523 <line x1="12" y1="16" x2="12.01" y2="16"></line> 524 </svg> 525 </div> 526 <h2>No se pudo conectar con YaCy</h2> 527 <p>Asegúrate de que YaCy esté en ejecución en <a href="http://localhost:8090" target="_blank" rel="noopener noreferrer">http://localhost:8090</a></p> 528 <div class="yacy-error-actions"> 529 <button class="retry-button large" on:click={() => fetchResults()}> 530 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 531 <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/> 532 </svg> 533 <span>Reintentar búsqueda</span> 534 </button> 535 <!-- Sin botón de datos de ejemplo --> 536 <!-- Sin modo debug --> 537 </div> 538 </div> 539 {:else if !isHomePage} 540 <!-- Mostrar cuando se navega a /search pero sin query --> 541 <div class="no-query-container"> 542 <div class="no-query-icon">🔍</div> 543 <h2>Ingresa tu búsqueda arriba</h2> 544 <p>Escribe lo que deseas buscar en la barra de búsqueda</p> 545 </div> 546 {/if} 547 </div> 548 549 <style> 550 /* Contenedor principal */ 551 .main-container { 552 display: flex; 553 flex-direction: column; 554 width: 100%; 555 box-sizing: border-box; 556 position: relative; 557 overflow-x: hidden; 558 } 559 560 /* Página de inicio */ 561 .home-view { 562 min-height: calc(100vh - var(--header-height)); 563 display: flex; 564 flex-direction: column; 565 justify-content: center; 566 padding: 0 var(--space-md); 567 } 568 569 /* Estilos de la página de inicio movidos a +layout.svelte */ 570 571 /* Página de resultados */ 572 .results-view { 573 min-height: calc(100vh - var(--header-height)); 574 padding-top: 0; /* Eliminado el padding extra */ 575 background-color: var(--bg-primary); 576 } 577 578 /* Contenedor para la vista de mapas */ 579 .map-view-container { 580 position: fixed; 581 width: 100vw; 582 height: calc(100vh - var(--header-height) - 50px); /* Ajustado para header + categorías */ 583 top: calc(var(--header-height) + 50px); /* Posicionado debajo del header y categorías */ 584 left: 0; 585 right: 0; 586 bottom: 0; 587 z-index: 50; 588 padding: 0; 589 margin: 0; 590 } 591 592 .results-section { 593 padding: 0 var(--space-md); 594 max-width: var(--content-max-width); 595 margin: 0 auto; 596 width: 100%; 597 box-sizing: border-box; 598 /* Usa el color de fondo del tema */ 599 } 600 601 .results-header { 602 margin-bottom: var(--space-md); 603 padding-bottom: var(--space-sm); 604 padding-top: var(--space-sm); 605 border-bottom: 1px solid var(--border-color); 606 } 607 608 .results-info { 609 font-size: 0.9rem; 610 color: var(--text-secondary); 611 } 612 613 .results-info p { 614 margin: 0; 615 } 616 617 .results-info strong { 618 color: var(--text-color); 619 font-weight: var(--font-weight-medium); 620 } 621 622 .search-time { 623 font-size: 0.85rem; 624 opacity: 0.8; 625 } 626 627 .results-status { 628 display: flex; 629 align-items: center; 630 gap: var(--space-sm); 631 color: var(--text-secondary); 632 font-size: 0.9rem; 633 } 634 635 .loader { 636 width: 16px; 637 height: 16px; 638 border: 2px solid var(--border-color); 639 border-top-color: var(--primary); 640 border-radius: 50%; 641 animation: spin 0.8s linear infinite; 642 } 643 644 @keyframes spin { 645 to { transform: rotate(360deg); } 646 } 647 648 .results-error { 649 display: flex; 650 align-items: flex-start; 651 gap: var(--space-md); 652 padding: var(--space-md); 653 background-color: rgba(239, 68, 68, 0.07); 654 border-radius: var(--border-radius); 655 border: 1px solid rgba(239, 68, 68, 0.15); 656 margin-bottom: var(--space-lg); 657 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); 658 } 659 660 .error-icon { 661 color: var(--error); 662 flex-shrink: 0; 663 } 664 665 .error-content { 666 flex: 1; 667 } 668 669 .error-content h3 { 670 color: var(--error); 671 font-size: 1rem; 672 margin: 0 0 var(--space-xs); 673 font-weight: var(--font-weight-medium); 674 } 675 676 .error-content p { 677 margin: 0 0 var(--space-md); 678 color: var(--text-color); 679 font-size: 0.9rem; 680 line-height: 1.5; 681 } 682 683 .error-help { 684 margin-bottom: var(--space-sm); 685 } 686 687 .error-help a { 688 color: var(--primary); 689 text-decoration: none; 690 font-size: 0.9rem; 691 display: inline-flex; 692 align-items: center; 693 gap: var(--space-xs); 694 font-weight: var(--font-weight-medium); 695 } 696 697 .error-help a:hover { 698 text-decoration: underline; 699 } 700 701 .error-actions { 702 display: flex; 703 gap: var(--space-sm); 704 } 705 706 .retry-button { 707 display: flex; 708 align-items: center; 709 gap: var(--space-xs); 710 background: var(--bg-secondary); 711 border: 1px solid var(--border-color); 712 border-radius: var(--border-radius-sm); 713 color: var(--text-color); 714 padding: var(--space-xs) var(--space-sm); 715 font-size: 0.9rem; 716 cursor: pointer; 717 transition: all var(--transition-speed); 718 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); 719 } 720 721 .retry-button:hover { 722 background: var(--bg-primary); 723 border-color: var(--primary); 724 color: var(--primary); 725 box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08); 726 } 727 728 .retry-button.large { 729 padding: var(--space-sm) var(--space-md); 730 font-size: 1rem; 731 } 732 733 /* Error de YaCy y sin consulta */ 734 .yacy-error-container, 735 .no-query-container { 736 display: flex; 737 flex-direction: column; 738 align-items: center; 739 justify-content: center; 740 text-align: center; 741 padding: var(--space-xxl) 0; 742 min-height: 50vh; 743 max-width: 600px; 744 margin: 0 auto; 745 } 746 747 .yacy-error-icon { 748 margin-bottom: var(--space-lg); 749 color: var(--error); 750 opacity: 0.7; 751 } 752 753 .yacy-error-container h2 { 754 margin: 0 0 var(--space-md); 755 font-size: 1.8rem; 756 font-weight: var(--font-weight-medium); 757 color: var(--text-color); 758 line-height: 1.3; 759 } 760 761 .yacy-error-container p, 762 .no-query-container p { 763 margin: 0 0 var(--space-lg); 764 max-width: 500px; 765 line-height: 1.5; 766 color: var(--text-secondary); 767 } 768 769 .yacy-error-container a { 770 color: var(--primary); 771 text-decoration: none; 772 font-weight: var(--font-weight-medium); 773 } 774 775 .yacy-error-container a:hover { 776 text-decoration: underline; 777 } 778 779 .yacy-error-actions { 780 display: flex; 781 gap: var(--space-md); 782 flex-wrap: wrap; 783 justify-content: center; 784 } 785 786 .no-query-icon { 787 font-size: 3rem; 788 margin-bottom: var(--space-md); 789 opacity: 0.7; 790 } 791 792 .no-query-container h2 { 793 margin: 0 0 var(--space-sm); 794 font-size: 1.5rem; 795 font-weight: var(--font-weight-medium); 796 color: var(--text-color); 797 } 798 799 800 /* Responsive */ 801 /* Control de vista sticky */ 802 .view-controls-sticky { 803 position: fixed; 804 left: var(--space-lg); 805 bottom: var(--space-lg); 806 z-index: 1000; 807 transition: transform 0.3s ease, opacity 0.3s ease; 808 animation: fade-in 0.5s ease-out; 809 } 810 811 /* Animación para entrada suave */ 812 @keyframes fade-in { 813 from { opacity: 0; transform: translateY(20px); } 814 to { opacity: 1; transform: translateY(0); } 815 } 816 817 @media (max-width: 1024px) { 818 .results-section { 819 padding: 0 var(--space-lg); 820 } 821 822 .view-controls-sticky { 823 left: var(--space-md); 824 bottom: var(--space-md); 825 } 826 827 .home-logo h1 { 828 font-size: 2.2rem; 829 } 830 831 .home-logo-img { 832 width: 100px; 833 height: 100px; 834 } 835 } 836 837 @media (max-width: 768px) { 838 .results-section { 839 padding: 0 var(--space-md); 840 } 841 842 .home-logo h1 { 843 font-size: 2rem; 844 } 845 846 .home-logo-img { 847 width: 90px; 848 height: 90px; 849 } 850 851 .home-tagline { 852 font-size: 1rem; 853 } 854 855 .yacy-error-actions { 856 flex-direction: column; 857 gap: var(--space-sm); 858 width: 100%; 859 max-width: 300px; 860 } 861 862 .retry-button.large, 863 .mock-button.large, 864 .debug-button.large { 865 width: 100%; 866 } 867 } 868 869 @media (max-width: 480px) { 870 .results-section { 871 padding: 0 var(--space-sm); 872 } 873 874 .home-logo h1 { 875 font-size: 1.8rem; 876 } 877 878 .home-logo-img { 879 width: 80px; 880 height: 80px; 881 } 882 883 .home-tagline { 884 font-size: 0.9rem; 885 margin-bottom: var(--space-lg); 886 } 887 888 .results-error { 889 flex-direction: column; 890 padding: var(--space-sm); 891 } 892 893 .error-actions { 894 flex-direction: column; 895 gap: var(--space-xs); 896 width: 100%; 897 } 898 899 .retry-button { 900 width: 100%; 901 justify-content: center; 902 } 903 } 904 </style>