Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252<script module lang="ts"> import { tv, type VariantProps } from "tailwind-variants";
export const topbar = tv({ slots: { nav: "h-10 w-full px-lg", row: "flex h-full items-center justify-between gap-md", brand: "flex shrink-0 items-center gap-xs", logo: "flex shrink-0 items-center gap-xs no-underline hover:no-underline", wordmark: "hidden sm:inline-flex", mark: "inline-flex sm:hidden", actions: "flex items-center", action: "flex items-center", divider: "hidden h-6 w-px self-center rounded-md bg-background-inset md:block", link: "flex items-center text-foreground-default transition-colors hover:text-foreground-muted", identity: "flex items-center gap-2xs", ball: "flex size-8 items-center justify-center rounded-full border border-border-default bg-background-inset text-foreground-placeholder md:size-5", bar: "hidden h-4 w-24 rounded-sm bg-background-inset md:block", switchHeader: "border-b-0 px-xs py-1.5 typography-paragraph-small text-foreground-muted", addBall: "flex size-5.25 shrink-0 items-center justify-center rounded-full bg-background-muted text-foreground-default" }, variants: { variant: { marketing: { actions: "gap-3.5" }, app: { nav: "border-b border-border-default bg-background-default", actions: "gap-xs" } } } });
export type TopbarVariants = VariantProps<typeof topbar>;</script>
<script lang="ts"> import { resolve } from "$app/paths"; import Button from "$lib/components/ui/Button.svelte"; import Dropdown from "$lib/components/ui/Dropdown.svelte"; import DropdownItem from "$lib/components/ui/DropdownItem.svelte"; import ArrowRight from "$icon/arrow-right"; import BookMarked from "$icon/book-marked"; import BookPlus from "$icon/book-plus"; import Building2 from "$icon/building-2"; import Import from "$icon/import"; import Cog from "$icon/cog"; import LineSquiggle from "$icon/line-squiggle"; import LogOut from "$icon/log-out"; import Plus from "$icon/plus"; import Search from "$icon/search"; import UserIcon from "$icon/user"; import Spinner from "$lib/components/ui/Spinner.svelte"; import Logo from "$lib/components/ui/Logo.svelte"; import User from "$lib/components/ui/User.svelte"; import TopbarSearch from "$lib/components/shell/TopbarSearch.svelte"; import NotificationBell from "$lib/components/notifications/NotificationBell.svelte"; import type { AuthAccount, CurrentUser } from "$lib/auth.svelte"; import Separator from "$lib/components/ui/Separator.svelte";
interface Props { variant: NonNullable<TopbarVariants["variant"]>; onSignOut?: () => void | Promise<void>; onSwitchAccount?: (did: AuthAccount["did"]) => void | Promise<void>; user: CurrentUser | null; accounts?: readonly AuthAccount[]; loginHref?: string; loading?: boolean; }
let { variant, onSignOut, onSwitchAccount, user, accounts = [], loginHref = "/login", loading = false }: Props = $props();
const appPath = (path: string) => resolve(path as "/");
const others = $derived(accounts.filter((account) => account.did !== user?.did));
const style = $derived(topbar({ variant }));</script>
<nav class={style.nav()} aria-label="main"> <div class={style.row()}> <div class={style.brand()}> <a href={resolve("/")} class={style.logo()}> <span class={style.wordmark()}> <Logo wordmark alpha class="h-6" /> </span> <span class={style.mark()}> <img src="/logos/dolly.svg" alt="tangled" class="size-8" /> </span> </a> </div>
<div class={style.actions()}> {#if variant === "marketing"} <Button variant="ghost" href={resolve("/login")}>Login</Button>
<span class="typography-paragraph-small text-foreground-muted">or</span>
<Button variant="primary" href={resolve("/signup")} insetShadow={true} icon={ArrowRight} iconSide="right" > Join now </Button> {:else} {#if user || loading} <div class={style.action({ class: "order-3 hidden sm:block md:order-1" })}> <TopbarSearch /> </div>
<div class={style.action({ class: "order-1 md:order-3" })}> <Dropdown insetShadow={true} size="small" id="new-menu" group="topbar" variant="button" align="right" label="New" > {#snippet trigger()} <Plus class="size-3.5" aria-hidden="true" /> <span class="hidden md:inline" aria-hidden="true">New</span> {/snippet} <DropdownItem href={resolve("/repo/new")} icon={BookPlus} >New repository</DropdownItem > <DropdownItem href={resolve("/strings/new")} icon={LineSquiggle} >New string</DropdownItem > <DropdownItem href={resolve("/organizations/new")} icon={Building2} >New organization</DropdownItem > <DropdownItem href={appPath("/repo/new?import=github")} icon={Import} >Import repository</DropdownItem > </Dropdown> </div>
<div class={style.action({ class: "order-2 md:order-4" })}> <NotificationBell /> </div>
<div class="order-3 md:hidden"> <Button variant="ghost" href={appPath("/search")} icon={Search} size="small" aria-hidden="true" aria-label="Search" /> </div>
<div class={style.divider({ class: "md:order-2" })}></div> {/if}
<div class={style.action({ class: "order-4 md:order-5" })}> {#if loading} <div class={style.identity()} role="status" aria-label="Signing in"> <span class={style.ball()}> <Spinner class="size-4" /> </span> <span class={style.bar()} aria-hidden="true"></span> </div> {:else if user} <Dropdown id="profile-menu" group="topbar" align="right" buttonVariant="ghost" triggerClass="pl-2xs" variant="button" size="small" > {#snippet trigger()} <User did={user.did} handle={user.handle} handleClass="hidden md:inline" /> {/snippet} {#if others.length > 0} <div class={style.switchHeader()}>Switch account</div> {#each others as account (account.did)} <DropdownItem onclick={() => onSwitchAccount?.(account.did)}> <User did={account.did} handle={account.handle} /> </DropdownItem> {/each} {/if} <DropdownItem href={appPath(loginHref)}> <span class={style.addBall()}> <Plus class="size-3" aria-hidden="true" /> </span> Add account </DropdownItem> <Separator /> <DropdownItem href={appPath(`/${user.handle}`)} icon={UserIcon} >Profile</DropdownItem > <DropdownItem href={appPath(`/${user.handle}?tab=repos`)} icon={BookMarked} > Repositories </DropdownItem> <DropdownItem href={appPath(`/${user.handle}?tab=strings`)} icon={LineSquiggle} > Strings </DropdownItem> <DropdownItem href={resolve("/settings")} icon={Cog} >Settings</DropdownItem > <DropdownItem danger icon={LogOut} onclick={onSignOut} >Logout</DropdownItem > </Dropdown> {:else} <div class={style.identity({ class: "gap-xs" })}> <Button variant="ghost" size="small" href={appPath(loginHref)} >Login</Button > <Button variant="primary" size="small" href={resolve("/signup")} insetShadow={true} > Join now </Button> </div> {/if} </div> {/if} </div> </div></nav>