/ src / components / Header.svelte
Header.svelte
  1  <script lang="ts">
  2    import { enhance } from "$app/forms";
  3    import { resolve } from "$app/paths";
  4    import Avatar from "$components/Avatar.svelte";
  5    import Logo from "$components/Logo.svelte";
  6    import * as Drawer from "$lib/components/ui/drawer";
  7    import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
  8    import type { User } from "$types/app";
  9  
 10    import Icon from "./Icon.svelte";
 11  
 12    interface Breadcrumb {
 13      label: string;
 14      href: string;
 15    }
 16  
 17    let {
 18      user,
 19      breadcrumbs,
 20      fqdn,
 21    }: {
 22      user: User | null;
 23      breadcrumbs: Breadcrumb[];
 24      fqdn: string;
 25    } = $props();
 26  
 27    let isLoggedIn = $derived(user !== null);
 28    let isMobileMenuOpen = $state(false);
 29  </script>
 30  
 31  {#snippet mobileMenu(user: User)}
 32    <Drawer.Root bind:open={isMobileMenuOpen}>
 33      <Drawer.Trigger>
 34        <div class="flex items-center gap-2">
 35          <div class="h-7 w-7">
 36            <Avatar
 37              email={user.email}
 38              username={user.handle}
 39              gravatarSize={128} />
 40          </div>
 41        </div>
 42      </Drawer.Trigger>
 43      <Drawer.Content class="p-3 shadow-4">
 44        <div class="flex flex-col gap-3">
 45          <div class="flex max-w-73.5 items-center gap-2.5">
 46            <div class="h-16 w-16 overflow-hidden rounded-md">
 47              <Avatar
 48                email={user.email}
 49                username={user.handle}
 50                gravatarSize={256}
 51                class="txt-heading-xxxl" />
 52            </div>
 53            <div class="flex flex-col gap-1">
 54              <div class="txt-heading-m line-clamp-1 break-all text-text-primary">
 55                {user.handle}.{fqdn}
 56              </div>
 57              <div
 58                class="txt-body-s-regular line-clamp-1 break-all text-text-tertiary">
 59                {user.email}
 60              </div>
 61            </div>
 62          </div>
 63          <div class="divider"></div>
 64          <a href={resolve(`/help`)} onclick={() => (isMobileMenuOpen = false)}>
 65            <div class="flex h-9 items-center gap-1">
 66              <Icon name="help" />
 67              Help
 68            </div>
 69          </a>
 70          <a
 71            href={resolve(`/settings`)}
 72            onclick={() => (isMobileMenuOpen = false)}>
 73            <div class="flex h-9 items-center gap-1">
 74              <Icon name="settings" />
 75              Settings
 76            </div>
 77          </a>
 78          <form method="POST" action="/logout" use:enhance>
 79            <button type="submit" class="w-full">
 80              <div class="flex h-9 items-center gap-1">
 81                <Icon name="disconnect" />
 82                Logout
 83              </div>
 84            </button>
 85          </form>
 86        </div>
 87      </Drawer.Content>
 88    </Drawer.Root>
 89  {/snippet}
 90  
 91  {#snippet desktopMenu(user: User)}
 92    <DropdownMenu.Root>
 93      <DropdownMenu.Trigger>
 94        <div class="h-7 w-7 cursor-pointer">
 95          <Avatar email={user.email} username={user.handle} gravatarSize={112} />
 96        </div>
 97      </DropdownMenu.Trigger>
 98      <DropdownMenu.Content align="end" class="rounded-sm shadow-4">
 99        <div class="flex flex-col gap-3">
100          <div class="flex max-w-73.5 items-center gap-2.5">
101            <div class="h-16 min-w-16 overflow-hidden rounded-md">
102              <Avatar
103                email={user.email}
104                username={user.handle}
105                gravatarSize={256}
106                class="txt-heading-xxxl" />
107            </div>
108            <div class="flex flex-col gap-1">
109              <div class="txt-heading-m line-clamp-1 break-all text-text-primary">
110                {user.handle}.{fqdn}
111              </div>
112              <div
113                class="txt-body-s-regular line-clamp-1 break-all text-text-tertiary">
114                {user.email}
115              </div>
116            </div>
117          </div>
118          <div class="divider"></div>
119          <a href={resolve(`/help`)}>
120            <DropdownMenu.Item class="flex h-9 items-center gap-1">
121              <Icon name="help" />
122              Help
123            </DropdownMenu.Item>
124          </a>
125          <a href={resolve(`/settings`)}>
126            <DropdownMenu.Item class="flex h-9 items-center gap-1">
127              <Icon name="settings" />
128              Settings
129            </DropdownMenu.Item>
130          </a>
131          <form method="POST" action="/logout" use:enhance>
132            <button type="submit" class="w-full">
133              <DropdownMenu.Item class="flex h-9 items-center gap-1">
134                <Icon name="disconnect" />
135                Logout
136              </DropdownMenu.Item>
137            </button>
138          </form>
139        </div>
140      </DropdownMenu.Content>
141    </DropdownMenu.Root>
142  {/snippet}
143  
144  <div class="flex items-center">
145    <div class="flex items-center gap-1.5 text-text-tertiary">
146      <a href={resolve("/")}>
147        <Logo />
148      </a>
149      <div>/</div>
150      <div class="flex items-center gap-2">
151        {#each breadcrumbs as { label }, i (i)}
152          <div>
153            {label}
154          </div>
155        {/each}
156      </div>
157    </div>
158    <div class="ml-auto">
159      {#if isLoggedIn && user}
160        <div class="hidden max-h-7 sm:block">
161          {@render desktopMenu(user)}
162        </div>
163        <div class="block sm:hidden">
164          {@render mobileMenu(user)}
165        </div>
166      {:else}
167        <a href={resolve("/login")}>Login</a>
168      {/if}
169    </div>
170  </div>