← back to Exo

dashboard/src/lib/components/HeaderNav.svelte

328 lines

<script lang="ts">
  import { browser } from "$app/environment";
  import { featureFlags } from "$lib/stores/app.svelte";

  const showAdvanced = $derived(featureFlags()["disaggregation"] === true);

  interface Props {
    showHome?: boolean;
    onHome?: (() => void) | null;
    showSidebarToggle?: boolean;
    sidebarVisible?: boolean;
    onToggleSidebar?: (() => void) | null;
    showMobileMenuToggle?: boolean;
    mobileMenuOpen?: boolean;
    onToggleMobileMenu?: (() => void) | null;
    showMobileRightToggle?: boolean;
    mobileRightOpen?: boolean;
    onToggleMobileRight?: (() => void) | null;
    downloadProgress?: {
      count: number;
      percentage: number;
    } | null;
  }

  let {
    showHome = true,
    onHome = null,
    showSidebarToggle = false,
    sidebarVisible = true,
    onToggleSidebar = null,
    showMobileMenuToggle = false,
    mobileMenuOpen = false,
    onToggleMobileMenu = null,
    showMobileRightToggle = false,
    mobileRightOpen = false,
    onToggleMobileRight = null,
    downloadProgress = null,
  }: Props = $props();

  function handleHome(): void {
    if (onHome) {
      onHome();
      return;
    }
    if (browser) {
      // Hash router: send to root
      window.location.hash = "/";
    }
  }

  function handleToggleSidebar(): void {
    if (onToggleSidebar) {
      onToggleSidebar();
    }
  }

  function handleToggleMobileMenu(): void {
    if (onToggleMobileMenu) {
      onToggleMobileMenu();
    }
  }

  function handleToggleMobileRight(): void {
    if (onToggleMobileRight) {
      onToggleMobileRight();
    }
  }
</script>

<header
  class="relative z-20 flex items-center justify-center px-4 md:px-6 pt-4 md:pt-8 pb-3 md:pb-4 bg-exo-dark-gray"
>
  <!-- Left: Sidebar Toggle (desktop) or Mobile Sidebar Toggle (mobile) -->
  <div
    class="absolute left-4 md:left-6 top-1/2 -translate-y-1/2 flex items-center gap-2"
  >
    <!-- Mobile sidebar toggle -->
    <button
      onclick={handleToggleMobileMenu}
      class="p-2 rounded border border-exo-light-gray/30 hover:border-exo-yellow/50 hover:bg-exo-medium-gray/30 transition-colors cursor-pointer md:hidden"
      title={mobileMenuOpen ? "Hide sidebar" : "Show sidebar"}
      aria-label={mobileMenuOpen
        ? "Hide conversation sidebar"
        : "Show conversation sidebar"}
      aria-pressed={mobileMenuOpen}
    >
      <svg
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        stroke-width="2"
        class="w-5 h-5 {mobileMenuOpen
          ? 'text-exo-yellow'
          : 'text-exo-light-gray'}"
      >
        {#if mobileMenuOpen}
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
          ></path>
        {:else}
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            d="M13 5l7 7-7 7M5 5l7 7-7 7"
          ></path>
        {/if}
      </svg>
    </button>
    <!-- Desktop sidebar toggle -->
    <button
      onclick={handleToggleSidebar}
      class="p-2 rounded border border-exo-light-gray/30 hover:border-exo-yellow/50 hover:bg-exo-medium-gray/30 transition-colors cursor-pointer hidden md:block"
      title={sidebarVisible ? "Hide sidebar" : "Show sidebar"}
      aria-label={sidebarVisible
        ? "Hide conversation sidebar"
        : "Show conversation sidebar"}
      aria-pressed={sidebarVisible}
    >
      <svg
        fill="none"
        viewBox="0 0 24 24"
        stroke="currentColor"
        stroke-width="2"
        class="w-5 h-5 {sidebarVisible
          ? 'text-exo-yellow'
          : 'text-exo-light-gray'}"
      >
        {#if sidebarVisible}
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
          ></path>
        {:else}
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            d="M13 5l7 7-7 7M5 5l7 7-7 7"
          ></path>
        {/if}
      </svg>
    </button>
  </div>

  <!-- Center: Logo (clickable to go home) -->
  <button
    onclick={handleHome}
    class="bg-transparent border-none outline-none focus:outline-none transition-opacity duration-200 hover:opacity-90 {showHome
      ? 'cursor-pointer'
      : 'cursor-default'}"
    title={showHome ? "Go to home" : ""}
    disabled={!showHome}
  >
    <img
      src="/exo-logo.png"
      alt="EXO"
      class="h-12 md:h-18 drop-shadow-[0_0_4px_rgba(255,215,0,0.3)]"
    />
  </button>

  <!-- Right: Home + Downloads + Mobile Right Toggle -->
  <nav
    class="absolute right-4 md:right-6 top-1/2 -translate-y-1/2 flex items-center gap-2 md:gap-4"
    aria-label="Main navigation"
  >
    <!-- Mobile right sidebar toggle (instances/models) - only show when not in chat mode -->
    {#if showMobileRightToggle}
      <button
        onclick={handleToggleMobileRight}
        class="p-2 rounded border border-exo-light-gray/30 hover:border-exo-yellow/50 hover:bg-exo-medium-gray/30 transition-colors cursor-pointer md:hidden"
        title={mobileRightOpen ? "Hide instances" : "Show instances"}
        aria-label={mobileRightOpen
          ? "Hide instances panel"
          : "Show instances panel"}
        aria-pressed={mobileRightOpen}
      >
        <svg
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
          stroke-width="2"
          class="w-5 h-5 {mobileRightOpen
            ? 'text-exo-yellow'
            : 'text-exo-light-gray'}"
        >
          {#if mobileRightOpen}
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              d="M13 5l7 7-7 7M5 5l7 7-7 7"
            ></path>
          {:else}
            <path
              stroke-linecap="round"
              stroke-linejoin="round"
              d="M11 19l-7-7 7-7m8 14l-7-7 7-7"
            ></path>
          {/if}
        </svg>
      </button>
    {/if}
    {#if showHome}
      <button
        onclick={handleHome}
        class="flex text-sm text-white/70 hover:text-exo-yellow transition-colors tracking-wider uppercase items-center gap-2 cursor-pointer"
        title="Back to topology view"
      >
        <svg
          class="w-4 h-4"
          fill="none"
          viewBox="0 0 24 24"
          stroke="currentColor"
        >
          <path
            stroke-linecap="round"
            stroke-linejoin="round"
            stroke-width="2"
            d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
          />
        </svg>
        <span class="hidden sm:inline">Home</span>
      </button>
    {/if}
    <a
      href="/#/downloads"
      class="text-xs md:text-sm text-white/70 hover:text-exo-yellow transition-colors tracking-wider uppercase flex items-center gap-1.5 md:gap-2 cursor-pointer"
      title="View downloads overview"
    >
      {#if downloadProgress}
        <!-- Compact download progress indicator -->
        <div class="relative w-4 h-4 flex-shrink-0">
          <svg class="w-4 h-4 -rotate-90" viewBox="0 0 20 20">
            <circle
              cx="10"
              cy="10"
              r="8"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              opacity="0.2"
            />
            <circle
              cx="10"
              cy="10"
              r="8"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-dasharray={2 * Math.PI * 8}
              stroke-dashoffset={2 *
                Math.PI *
                8 *
                (1 - downloadProgress.percentage / 100)}
              class="text-blue-400 transition-all duration-300"
            />
          </svg>
          <div
            class="absolute inset-0 flex items-center justify-center text-[6px] font-mono text-blue-400"
          >
            {downloadProgress.count}
          </div>
        </div>
      {:else}
        <svg
          class="w-4 h-4"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <path d="M12 3v12" />
          <path d="M7 12l5 5 5-5" />
          <path d="M5 21h14" />
        </svg>
      {/if}
      <span class="hidden sm:inline">Downloads</span>
    </a>
    <a
      href="/#/integrations"
      class="text-xs md:text-sm text-white/70 hover:text-exo-yellow transition-colors tracking-wider uppercase flex items-center gap-1.5 md:gap-2 cursor-pointer"
      title="Integration configs for external tools"
    >
      <svg
        class="w-4 h-4"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
        <path
          d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"
        />
      </svg>
      <span class="hidden sm:inline">Integrations</span>
    </a>
    {#if showAdvanced}
      <a
        href="/#/advanced"
        class="text-xs md:text-sm text-white/70 hover:text-exo-yellow transition-colors tracking-wider uppercase flex items-center gap-1.5 md:gap-2 cursor-pointer"
        title="Advanced cluster settings"
      >
        <svg
          class="w-4 h-4"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <circle cx="12" cy="12" r="3" />
          <path
            d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
          />
        </svg>
        <span class="hidden sm:inline">Advanced</span>
      </a>
    {/if}
  </nav>
</header>