Navbar.svelte 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. <script lang="ts">
  2. import type { Snippet } from 'svelte';
  3. import { cn } from '$lib/utils/cn.js';
  4. import ThemeToggle from './ThemeToggle.svelte';
  5. interface Props {
  6. class?: string;
  7. logo?: Snippet;
  8. nav?: Snippet;
  9. actions?: Snippet;
  10. hideThemeToggle?: boolean;
  11. }
  12. let {
  13. class: className = '',
  14. logo,
  15. nav,
  16. actions,
  17. hideThemeToggle = false,
  18. }: Props = $props();
  19. let menuOpen = $state(false);
  20. </script>
  21. <header
  22. class={cn(
  23. 'sticky top-0 z-40 h-14 w-full',
  24. 'bg-[var(--card)]/80 backdrop-blur-xl',
  25. 'border-b border-[var(--border)]',
  26. className
  27. )}
  28. >
  29. <div class="mx-auto flex h-full w-full max-w-7xl items-center gap-4 px-4 sm:px-6 lg:px-8">
  30. <!-- Logo -->
  31. {#if logo}
  32. <div class="flex shrink-0 items-center">
  33. {@render logo()}
  34. </div>
  35. {/if}
  36. <!-- Desktop nav -->
  37. {#if nav}
  38. <nav class="hidden md:flex items-center gap-1 flex-1">
  39. {@render nav()}
  40. </nav>
  41. {:else}
  42. <div class="flex-1"></div>
  43. {/if}
  44. <!-- Actions + theme toggle -->
  45. <div class="flex items-center gap-1">
  46. {#if actions}
  47. {@render actions()}
  48. {/if}
  49. {#if !hideThemeToggle}
  50. <ThemeToggle />
  51. {/if}
  52. <!-- Mobile menu button -->
  53. {#if nav}
  54. <button
  55. type="button"
  56. aria-label="Toggle menu"
  57. aria-expanded={menuOpen}
  58. onclick={() => { menuOpen = !menuOpen; }}
  59. class={cn(
  60. 'md:hidden flex h-8 w-8 items-center justify-center rounded-[var(--radius)]',
  61. 'cursor-pointer text-[var(--muted-foreground)] hover:text-[var(--foreground)] hover:bg-[var(--muted)]',
  62. 'transition-all duration-[150ms]',
  63. 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ring)]',
  64. )}
  65. >
  66. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  67. {#if menuOpen}
  68. <path d="M18 6L6 18M6 6l12 12"/>
  69. {:else}
  70. <path d="M4 6h16M4 12h16M4 18h16"/>
  71. {/if}
  72. </svg>
  73. </button>
  74. {/if}
  75. </div>
  76. </div>
  77. <!-- Mobile nav dropdown -->
  78. {#if nav && menuOpen}
  79. <div
  80. class="md:hidden border-t border-[var(--border)] bg-[var(--card)]/95 backdrop-blur-xl"
  81. >
  82. <div class="mx-auto max-w-7xl px-4 py-3 flex flex-col gap-1">
  83. {@render nav()}
  84. </div>
  85. </div>
  86. {/if}
  87. </header>