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>