+layout.svelte
1 <script lang="ts"> 2 import SearchBar from './components/SearchBar.svelte'; 3 import ViewModeSelector from './components/ViewModeSelector.svelte'; 4 import PageTransition from '$lib/components/PageTransition.svelte'; 5 import { page } from '$app/stores'; 6 import { onMount, onDestroy, afterUpdate } from 'svelte'; 7 import { derived } from 'svelte/store'; 8 import '$lib/styles.css'; // Import global styles 9 10 export let data; 11 12 // Extraer datos de la URL para su uso reactivo 13 $: pageUrl = $page.url; 14 $: hasQueryParam = pageUrl.searchParams.has('query'); 15 16 // Detectar si estamos en la página principal (/) o mostrando resultados de búsqueda 17 // La detección se hace ahora solo basada en la presencia del parámetro query 18 $: isHomePage = !hasQueryParam; 19 export {}; 20 21 // Manejar transiciones de página - derivar de page.url para crear una clave única 22 // para las transiciones 23 const pageTransitionKey = derived(page, (pageStore) => { 24 return pageStore.url.pathname + '?' + pageStore.url.searchParams; 25 }); 26 27 // Estado de navegación para animaciones 28 let isNavigating = false; 29 let navigationTimeout: number | undefined; 30 31 // Indicador de primera carga para deshabilitar animaciones la primera vez 32 let isFirstLoad = true; 33 34 // Función para manejar transiciones de navegación 35 function handleNavigation() { 36 if (!isFirstLoad) { 37 isNavigating = true; 38 39 // Permitir que terminen las animaciones antes de restablecer 40 if (navigationTimeout) clearTimeout(navigationTimeout); 41 navigationTimeout = window.setTimeout(() => { 42 isNavigating = false; 43 }, 500); // Duración ligeramente mayor que la animación más larga 44 } 45 } 46 47 // Detectar cambios en la URL para aplicar transiciones 48 // Reaccionar a cambios en la URL 49 $: if (pageUrl) { 50 if (!isFirstLoad) { 51 handleNavigation(); 52 } 53 } 54 55 // Inicializar 56 onMount(() => { 57 // Habilitar transiciones después de la carga inicial 58 setTimeout(() => { 59 document.body.classList.add('transitions-enabled'); 60 isFirstLoad = false; 61 }, 300); 62 63 // Evento para detectar inicio de navegación 64 const handleBeforeNavigate = () => { 65 handleNavigation(); 66 }; 67 68 // Añadir listener para eventos de navegación SvelteKit 69 window.addEventListener('sveltekit:navigation-start', handleBeforeNavigate); 70 71 return () => { 72 window.removeEventListener('sveltekit:navigation-start', handleBeforeNavigate); 73 }; 74 }); 75 76 onDestroy(() => { 77 if (navigationTimeout) clearTimeout(navigationTimeout); 78 }); 79 80 // Hacer scroll al principio cuando cambia la página 81 afterUpdate(() => { 82 if (isNavigating && !isFirstLoad) { 83 window.scrollTo({ top: 0, behavior: 'smooth' }); 84 } 85 }); 86 </script> 87 88 <div class="app-container {isNavigating ? 'is-navigating' : ''}"> 89 {#if isHomePage} 90 <!-- Página de inicio - Barra de búsqueda centrada --> 91 <div class="home-container"> 92 <div class="home-hero"> 93 <div class="home-logo-large" in:fly={{ y: -30, duration: 600, delay: 100 }}> 94 <img src="/logo.png" alt="Logo" class="logo-img-large" /> 95 <h1 class="home-title">CelestiumOS</h1> 96 </div> 97 <p class="home-tagline" in:fly={{ y: 10, duration: 500, delay: 200 }}>El buscador que respeta tu privacidad</p> 98 <p class="home-privacy" in:fly={{ y: 10, duration: 500, delay: 300 }}> 99 <span class="privacy-feature">✓ Sin rastreadores</span> 100 <span class="privacy-feature">✓ Sin registro de búsquedas</span> 101 <span class="privacy-feature">✓ Sin perfiles de usuario</span> 102 </p> 103 <div class="home-search" in:fly={{ y: 20, duration: 500, delay: 300 }}> 104 <SearchBar /> 105 </div> 106 </div> 107 </div> 108 {:else} 109 <!-- Página de resultados - Utilizar el componente SearchBar que ya contiene la barra de navegación --> 110 <div class="search-header-wrapper"> 111 <SearchBar /> 112 113 <!-- Controles de vista (columnas) ahora ubicados en la parte inferior izquierda --> 114 <!-- Los controles se eliminan de aquí y se moverán a +page.svelte para la página de resultados --> 115 </div> 116 {/if} 117 118 <main class="app-main {isHomePage ? 'home-main' : 'results-main'}" style="margin-top: {isHomePage ? '0' : 'calc(var(--header-height) + 50px)'};"> 119 <PageTransition key={$pageTransitionKey}> 120 <slot /> 121 </PageTransition> 122 </main> 123 124 {#if isHomePage || !hasQueryParam} 125 <footer class="app-footer"> 126 <div class="footer-content"> 127 <p>© 2025 CelestiumOS. Todos los derechos reservados.</p> 128 <div class="footer-links"> 129 <a href="/about" aria-label="Acerca de">Acerca de</a> 130 <a href="/privacy" aria-label="Privacidad">Privacidad</a> 131 <a href="/terms" aria-label="Términos">Términos</a> 132 <a href="/contact" aria-label="Contacto">Contacto</a> 133 </div> 134 </div> 135 </footer> 136 {/if} 137 </div> 138 139 <style> 140 /* Transiciones suaves */ 141 :global(body) { 142 transition: none; /* Deshabilitar transiciones inicialmente */ 143 } 144 145 :global(body.transitions-enabled) { 146 transition: background-color 0.3s ease, color 0.3s ease; /* Habilitar solo después de carga */ 147 } 148 149 :global(body.transitions-enabled *) { 150 transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; 151 } 152 153 /* Aplicar efecto de desenfoque durante la navegación */ 154 .app-container.is-navigating :global(:not(.page-transition)) { 155 filter: blur(0.5px); 156 opacity: 0.98; 157 transition: filter 0.3s ease, opacity 0.3s ease; 158 } 159 160 /* Añadir transiciones suaves */ 161 .app-container { 162 transition: filter 0.3s ease, opacity 0.3s ease; 163 } 164 165 /* Mejoras para controles de vista */ 166 .view-controls { 167 position: fixed; 168 top: calc(var(--header-height) + 12px); 169 right: var(--space-xl); 170 z-index: 1000; 171 box-shadow: 0 2px 8px rgba(0,0,0,0.15); 172 border-radius: var(--border-radius); 173 animation: pulse-glow 2s infinite alternate; 174 } 175 176 @keyframes pulse-glow { 177 0% { 178 box-shadow: 0 2px 8px rgba(0,0,0,0.1); 179 } 180 100% { 181 box-shadow: 0 2px 12px rgba(var(--primary-rgb), 0.5); 182 } 183 } 184 185 /* Ajuste para asegurar que --primary-rgb está disponible */ 186 :root { 187 --primary-rgb: 34, 106, 243; /* Valor por defecto que se sobrescribirá */ 188 } 189 190 /* Contenedor principal */ 191 .app-container { 192 display: flex; 193 flex-direction: column; 194 min-height: 100vh; 195 background: var(--bg-primary); 196 color: var(--text-color); 197 font-family: var(--font-primary); 198 overflow-x: hidden; /* Evitar scroll horizontal */ 199 } 200 201 /* Header */ 202 .app-header { 203 display: flex; 204 align-items: center; 205 justify-content: space-between; 206 padding: 0 var(--space-xl); 207 background: var(--bg-primary); 208 border-bottom: 1px solid var(--border-color); 209 position: sticky; 210 top: 0; 211 z-index: 100; 212 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03); 213 height: var(--header-height); 214 transition: all var(--transition-speed); 215 } 216 217 .home-header { 218 background: transparent; 219 border: none; 220 box-shadow: none; 221 } 222 223 .header-left, 224 .header-right { 225 display: flex; 226 align-items: center; 227 } 228 229 .header-left { 230 flex: 1; 231 min-width: 120px; 232 } 233 234 .logo { 235 display: flex; 236 align-items: center; 237 gap: var(--space-sm); 238 text-decoration: none; 239 transition: opacity var(--transition-speed); 240 } 241 242 .logo:hover { 243 opacity: 0.9; 244 } 245 246 .logo-img { 247 width: 36px; 248 height: 36px; 249 object-fit: contain; 250 } 251 252 .logo-text { 253 font-size: 1.25rem; 254 font-weight: var(--font-weight-semibold); 255 margin: 0; 256 color: var(--primary); 257 } 258 259 .header-right { 260 flex: 1; 261 display: flex; 262 justify-content: flex-end; 263 gap: var(--space-md); 264 min-width: 120px; 265 } 266 267 /* Página de inicio */ 268 .home-container { 269 display: flex; 270 flex-direction: column; 271 justify-content: center; 272 align-items: center; 273 padding: var(--space-md) var(--space-lg); 274 margin-top: 0; 275 min-height: 60vh; /* Suficiente altura para centrar verticalmente */ 276 position: relative; 277 } 278 279 .home-hero { 280 display: flex; 281 flex-direction: column; 282 align-items: center; 283 max-width: 650px; 284 width: 100%; 285 text-align: center; 286 justify-content: center; 287 } 288 289 .home-logo-large { 290 display: flex; 291 flex-direction: column; 292 align-items: center; 293 margin-bottom: var(--space-md); 294 } 295 296 .logo-img-large { 297 width: 140px; /* Más pequeño como Qwant */ 298 height: 140px; 299 margin-bottom: var(--space-sm); 300 object-fit: contain; 301 } 302 303 .home-title { 304 font-size: 2.5rem; 305 font-weight: var(--font-weight-bold); 306 margin: var(--space-md) 0 0; 307 color: var(--primary); 308 letter-spacing: -0.03em; 309 } 310 311 .home-tagline { 312 font-size: 1.1rem; 313 color: var(--text-color); 314 font-weight: 500; 315 margin: var(--space-md) 0 var(--space-md); 316 max-width: 80%; 317 line-height: 1.4; 318 } 319 320 .home-privacy { 321 display: flex; 322 flex-wrap: wrap; 323 justify-content: center; 324 gap: var(--space-lg); 325 margin-bottom: var(--space-lg); 326 color: var(--text-secondary); 327 font-size: 0.95rem; 328 } 329 330 .privacy-feature { 331 display: flex; 332 align-items: center; 333 gap: var(--space-xs); 334 } 335 336 .privacy-feature::before { 337 content: "✓"; 338 color: var(--primary); 339 font-weight: bold; 340 } 341 342 .home-search { 343 width: 100%; 344 max-width: 600px; 345 transition: all 0.3s ease; 346 } 347 348 .home-search:focus-within { 349 transform: scale(1.02); 350 } 351 352 /* Sección de búsqueda para páginas de resultados */ 353 .search-container { 354 padding: var(--space-md) var(--space-lg); 355 background: var(--bg-secondary); 356 border-bottom: 1px solid var(--border-color); 357 box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); 358 display: flex; 359 justify-content: center; 360 align-items: center; 361 position: relative; 362 z-index: 10; 363 } 364 365 .search-section { 366 display: flex; 367 flex-direction: column; 368 gap: var(--space-sm); 369 width: 100%; 370 max-width: 900px; 371 margin: 0 auto; 372 } 373 374 .search-filters { 375 display: flex; 376 justify-content: space-between; 377 width: 100%; 378 } 379 380 .filters-group { 381 display: flex; 382 gap: var(--space-md); 383 flex-wrap: wrap; 384 width: 100%; 385 justify-content: flex-end; 386 } 387 388 /* Barra de navegación fija */ 389 .sticky-search-nav { 390 position: fixed; 391 bottom: 0; 392 left: 0; 393 right: 0; 394 background: var(--bg-primary); 395 border-top: 1px solid var(--border-color); 396 box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.08); 397 z-index: 50; 398 height: 56px; 399 transform: translateY(100%); 400 transition: transform 0.3s ease; 401 display: flex; 402 align-items: center; 403 justify-content: center; 404 } 405 406 .sticky-search-nav.visible { 407 transform: translateY(0); 408 } 409 410 .sticky-search-container { 411 display: flex; 412 justify-content: space-between; 413 align-items: center; 414 width: 100%; 415 max-width: 900px; 416 padding: 0 var(--space-md); 417 } 418 419 .sticky-search-types { 420 display: flex; 421 gap: var(--space-sm); 422 overflow-x: auto; 423 scrollbar-width: none; 424 -ms-overflow-style: none; 425 padding: var(--space-xs) 0; 426 } 427 428 .sticky-search-types::-webkit-scrollbar { 429 display: none; 430 } 431 432 .sticky-type-button { 433 display: flex; 434 align-items: center; 435 gap: var(--space-xs); 436 background: transparent; 437 border: none; 438 color: var(--text-secondary); 439 font-size: 0.85rem; 440 padding: var(--space-xs) var(--space-sm); 441 border-radius: var(--border-radius-sm); 442 white-space: nowrap; 443 cursor: pointer; 444 transition: all var(--transition-speed); 445 } 446 447 .sticky-type-button:hover { 448 color: var(--primary); 449 background: rgba(var(--primary-rgb), 0.05); 450 } 451 452 .sticky-type-button.active { 453 color: var(--primary); 454 background: rgba(var(--primary-rgb), 0.1); 455 font-weight: var(--font-weight-medium); 456 } 457 458 .sticky-type-button svg { 459 opacity: 0.8; 460 } 461 462 .sticky-search-actions { 463 display: flex; 464 align-items: center; 465 gap: var(--space-sm); 466 } 467 468 .theme-controls { 469 display: flex; 470 align-items: center; 471 gap: var(--space-xs); 472 } 473 474 /* Main y Footer */ 475 .app-main { 476 flex: 1; 477 padding: var(--space-md) var(--space-xl); /* Reducido de lg a md */ 478 max-width: var(--content-max-width); 479 margin: 0 auto; 480 width: 100%; 481 box-sizing: border-box; 482 overflow-x: hidden; 483 padding-bottom: var(--space-md); /* Reducido para minimizar espacio */ 484 display: flex; 485 flex-direction: column; 486 } 487 488 .home-main { 489 padding: 0; 490 display: flex; 491 justify-content: center; 492 flex: 0 1 auto; 493 max-height: 55vh; /* Reducido para dejar más espacio al footer */ 494 } 495 496 .app-footer { 497 background: var(--bg-primary); 498 padding: var(--space-sm) var(--space-lg); 499 border-top: 1px solid var(--border-color); 500 position: static; 501 padding-bottom: var(--space-sm); 502 width: 100%; 503 /* Sin margen para evitar problemas con el scroll y detección duplicada */ 504 } 505 506 .footer-content { 507 max-width: var(--content-max-width); 508 margin: 0 auto; 509 display: flex; 510 justify-content: space-between; 511 align-items: center; 512 flex-wrap: wrap; 513 gap: var(--space-md); 514 } 515 516 .footer-content p { 517 font-size: 0.85rem; 518 color: var(--text-secondary); 519 margin: 0; 520 } 521 522 .footer-links { 523 display: flex; 524 gap: var(--space-xl); 525 } 526 527 .footer-links a { 528 font-size: 0.85rem; 529 color: var(--text-secondary); 530 text-decoration: none; 531 transition: all var(--transition-speed); 532 padding: var(--space-xs) 0; 533 position: relative; 534 } 535 536 .footer-links a:hover { 537 color: var(--primary); 538 } 539 540 .footer-links a:hover::after { 541 content: ""; 542 position: absolute; 543 left: 0; 544 bottom: 0; 545 width: 100%; 546 height: 1px; 547 background-color: var(--primary); 548 transform: scaleX(1); 549 transform-origin: left; 550 transition: transform var(--transition-speed); 551 } 552 553 /* Responsividad */ 554 @media (max-width: 1024px) { 555 .app-main { 556 padding: var(--space-md) var(--space-lg); 557 padding-bottom: var(--space-md); 558 } 559 } 560 561 @media (max-width: 768px) { 562 .app-header { 563 padding: 0 var(--space-md); 564 } 565 566 .home-container { 567 padding: var(--space-md) var(--space-md); 568 min-height: 50vh; 569 } 570 571 .logo-img-large { 572 width: 120px; 573 height: 120px; 574 } 575 576 .home-title { 577 font-size: 2.2rem; 578 } 579 580 .home-tagline { 581 font-size: 1rem; 582 } 583 584 .home-privacy { 585 gap: var(--space-md); 586 font-size: 0.9rem; 587 flex-direction: column; 588 align-items: center; 589 } 590 591 .search-container { 592 padding: var(--space-sm) var(--space-md); 593 } 594 595 .search-section { 596 gap: var(--space-xs); 597 } 598 599 .search-filters { 600 flex-direction: column; 601 gap: var(--space-xs); 602 } 603 604 .filters-group { 605 justify-content: flex-start; 606 } 607 608 .app-main { 609 padding: var(--space-md); 610 padding-bottom: var(--space-md); 611 } 612 613 .app-footer { 614 padding: var(--space-sm) var(--space-md); 615 padding-bottom: var(--space-sm); 616 } 617 618 .footer-content { 619 flex-direction: column; 620 text-align: center; 621 gap: var(--space-md); 622 } 623 624 .footer-links { 625 margin: 0 auto; 626 flex-wrap: wrap; 627 justify-content: center; 628 gap: var(--space-lg); 629 } 630 631 /* Ajustes para barra fija en móvil */ 632 .sticky-type-button span { 633 font-size: 0.75rem; 634 } 635 } 636 637 @media (max-width: 480px) { 638 .app-header { 639 flex-wrap: wrap; 640 gap: var(--space-xs); 641 height: auto; 642 padding: var(--space-xs) var(--space-sm); 643 } 644 645 .header-left { 646 flex: 1 0 100%; 647 justify-content: center; 648 margin-bottom: var(--space-xs); 649 } 650 651 .header-right { 652 flex: 1 0 100%; 653 justify-content: center; 654 } 655 656 .home-container { 657 padding: var(--space-sm) var(--space-md); 658 } 659 660 .logo-img-large { 661 width: 150px; /* Reducido de 180px */ 662 height: 150px; /* Reducido de 180px */ 663 } 664 665 .home-title { 666 font-size: 2.5rem; /* Reducido de 3rem */ 667 } 668 669 .home-logo-img { 670 width: 72px; 671 height: 72px; 672 } 673 674 .app-main { 675 padding: var(--space-sm) var(--space-xs); 676 padding-bottom: var(--space-sm); 677 } 678 679 .search-container { 680 padding: var(--space-xs) var(--space-xs); 681 } 682 683 .search-filters { 684 padding: 0 var(--space-xs); 685 } 686 687 .app-footer { 688 padding: var(--space-xs) var(--space-md); 689 padding-bottom: var(--space-xs); 690 } 691 692 /* Ajustes para barra fija en móvil */ 693 .sticky-type-button span { 694 display: none; /* Ocultar texto, mostrar solo iconos */ 695 } 696 697 .sticky-type-button { 698 padding: var(--space-xs); 699 width: 36px; 700 height: 36px; 701 display: flex; 702 align-items: center; 703 justify-content: center; 704 } 705 706 .theme-controls { 707 scale: 0.9; /* Reducir ligeramente el tamaño */ 708 } 709 } 710 711 /* Optimizaciones para pantallas muy pequeñas */ 712 @media (max-width: 350px) { 713 .app-header { 714 padding: var(--space-xs) 4px; 715 } 716 717 .app-main { 718 padding: var(--space-xs) 4px; 719 padding-bottom: var(--space-xs); 720 } 721 722 .search-container { 723 padding: var(--space-xs) 4px; 724 } 725 726 .logo-img-large { 727 width: 120px; /* Reducido de 150px */ 728 height: 120px; /* Reducido de 150px */ 729 } 730 731 .home-title { 732 font-size: 2.2rem; /* Reducido de 2.5rem */ 733 } 734 735 .logo-text { 736 font-size: 1.1rem; 737 } 738 739 .footer-links { 740 gap: var(--space-md); 741 } 742 743 .footer-links a { 744 font-size: 0.75rem; 745 } 746 747 /* Miniaturización extrema para pantallas muy pequeñas */ 748 .sticky-search-container { 749 padding: 0 var(--space-xs); 750 } 751 752 .sticky-type-button { 753 width: 32px; 754 height: 32px; 755 } 756 757 .sticky-type-button svg { 758 width: 14px; 759 height: 14px; 760 } 761 } 762 </style>