This repository has no description
1<script module lang="ts">
2 import { tv, type VariantProps } from "tailwind-variants";
3
4 export const topbar = tv({
5 slots: {
6 nav: "h-11.5 w-full px-5",
7 row: "flex h-full items-center justify-between gap-4",
8 brand: "flex shrink-0 items-center gap-2",
9 logo: "flex shrink-0 items-center gap-2 no-underline hover:no-underline",
10 wordmark: "hidden sm:inline-flex",
11 mark: "inline-flex sm:hidden",
12 actions: "flex items-center",
13 action: "flex items-center",
14 divider: "mx-2 hidden h-6 w-px self-center rounded-md bg-background-inset md:block",
15 link: "flex items-center text-foreground-default transition-colors hover:text-foreground-muted",
16 identity: "flex items-center",
17 ball: "size-8 rounded-full bg-background-inset md:size-5",
18 bar: "hidden h-4 w-24 rounded-sm bg-background-inset md:block"
19 },
20 variants: {
21 variant: {
22 marketing: { actions: "gap-3.5" },
23 app: {
24 nav: "border-b border-border-default bg-background-default",
25 actions: "gap-2"
26 }
27 },
28 // The signing-in placeholder wraps a spinner, so its ball gets a border and tighter gap.
29 pending: {
30 true: {
31 identity: "gap-1",
32 ball: "flex items-center justify-center border border-border-default text-foreground-placeholder"
33 },
34 false: { identity: "gap-2" }
35 }
36 },
37 defaultVariants: {
38 pending: false
39 }
40 });
41
42 export type TopbarVariants = VariantProps<typeof topbar>;
43</script>
44
45<script lang="ts">
46 import { resolve } from "$app/paths";
47 import Button from "../ui/Button.svelte";
48 import Dropdown from "../ui/Dropdown.svelte";
49 import DropdownItem from "../ui/DropdownItem.svelte";
50 import ArrowRight from "$icon/arrow-right";
51 import Bell from "$icon/bell";
52 import BookMarked from "$icon/book-marked";
53 import BookPlus from "$icon/book-plus";
54 import Cog from "$icon/cog";
55 import LineSquiggle from "$icon/line-squiggle";
56 import LogOut from "$icon/log-out";
57 import Plus from "$icon/plus";
58 import Search from "$icon/search";
59 import UserIcon from "$icon/user";
60 import Spinner from "../ui/Spinner.svelte";
61 import Logo from "../ui/Logo.svelte";
62 import User from "../ui/User.svelte";
63 import TopbarSearch from "./TopbarSearch.svelte";
64 import type { CurrentUser } from "$lib/auth.svelte";
65 import Separator from "../ui/Separator.svelte";
66
67 interface Props {
68 variant: NonNullable<TopbarVariants["variant"]>;
69 onSignOut?: () => void | Promise<void>;
70 user: CurrentUser | null;
71 loading?: boolean;
72 }
73
74 let { variant, onSignOut, user, loading = false }: Props = $props();
75
76 const appPath = (path: string) => resolve(path as "/");
77
78 const style = $derived(topbar({ variant }));
79</script>
80
81<nav class={style.nav()} aria-label="main">
82 <div class={style.row()}>
83 <div class={style.brand()}>
84 <a href={resolve("/")} class={style.logo()}>
85 <span class={style.wordmark()}>
86 <Logo wordmark alpha class="h-6.5" />
87 </span>
88 <span class={style.mark()}>
89 <img src="/logos/dolly.svg" alt="tangled" class="size-8" />
90 </span>
91 </a>
92 </div>
93
94 <div class={style.actions()}>
95 {#if variant === "marketing"}
96 <TopbarSearch />
97
98 <div class={style.divider()}></div>
99
100 <Button variant="ghost" href={resolve("/login")}>Login</Button>
101
102 <span class="typography-paragraph-small text-foreground-muted">or</span>
103
104 <Button variant="primary" href={resolve("/signup")} insetShadow={true}>
105 <span>Join now</span>
106 <ArrowRight class="size-3" aria-hidden="true" />
107 </Button>
108 {:else}
109 {#if user || loading}
110 <div class={style.action({ class: "order-3 hidden sm:block md:order-1" })}>
111 <TopbarSearch />
112 </div>
113
114 <div class={style.action({ class: "order-1 md:order-3" })}>
115 <Dropdown
116 insetShadow={true}
117 size="sm"
118 id="new-menu"
119 group="topbar"
120 variant="button"
121 align="right"
122 >
123 {#snippet trigger()}
124 <Plus class="size-3.5" aria-hidden="true" />
125 <span class="hidden md:inline">New</span>
126 {/snippet}
127 <DropdownItem href="/repo/new" icon={BookPlus}>New repository</DropdownItem>
128 <DropdownItem href="/strings/new" icon={LineSquiggle}>New string</DropdownItem>
129 </Dropdown>
130 </div>
131
132 <div class={style.action({ class: "order-2 md:order-4" })}>
133 <Button
134 variant="ghost"
135 href={appPath("/notifications")}
136 icon={Bell}
137 size="sm"
138 aria-hidden="true"
139 aria-label="Notifications"
140 />
141 </div>
142
143 <div class="order-3 md:hidden">
144 <Button
145 variant="ghost"
146 href={appPath("/search")}
147 icon={Search}
148 size="sm"
149 aria-hidden="true"
150 aria-label="Search"
151 />
152 </div>
153
154 <div class={style.divider({ class: "md:order-2" })}></div>
155 {/if}
156
157 <div class={style.action({ class: "order-4 md:order-5" })}>
158 {#if loading}
159 <div class={style.identity({ pending: true })} role="status" aria-label="Signing in">
160 <span class={style.ball({ pending: true })}>
161 <Spinner class="size-4" />
162 </span>
163 <span class={style.bar()} aria-hidden="true"></span>
164 </div>
165 {:else if user}
166 <Dropdown
167 id="profile-menu"
168 group="topbar"
169 align="right"
170 variant="button"
171 buttonVariant="ghost"
172 size="sm"
173 >
174 {#snippet trigger()}
175 <User did={user.did} handle={user.handle} />
176 {/snippet}
177 <DropdownItem href={`/${user.handle}`} icon={UserIcon}>Profile</DropdownItem>
178 <DropdownItem href={`/${user.handle}?tab=repos`} icon={BookMarked}>
179 Repositories
180 </DropdownItem>
181 <DropdownItem href={`/${user.handle}?tab=strings`} icon={LineSquiggle}>
182 Strings
183 </DropdownItem>
184 <DropdownItem href="/settings" icon={Cog}>Settings</DropdownItem>
185 <Separator />
186 <DropdownItem danger icon={LogOut} onclick={onSignOut}>Logout</DropdownItem>
187 </Dropdown>
188 {:else}
189 <div class={style.identity()} aria-hidden="true">
190 <span class={style.ball()}></span>
191 <span class={style.bar()}></span>
192 </div>
193 {/if}
194 </div>
195 {/if}
196 </div>
197 </div>
198</nav>