Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282<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-md 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: { nav: "mx-auto h-14 max-w-page rounded-2xl border border-border-default bg-background-default/80 px-sm shadow-small backdrop-blur-md", actions: "gap-xs" }, app: { nav: "border-b border-border-default bg-background-default", actions: "gap-xs" } } } });
export type TopbarVariants = VariantProps<typeof topbar>;</script>
<script lang="ts"> import { profileIdentifier } from "#lib/api/identity.ts"; import { resolve } from "$app/paths"; import type { ResolvedPathname } from "$app/types"; 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 DropdownSubmenu from "#lib/components/ui/DropdownSubmenu.svelte"; import DropdownGroup from "#lib/components/ui/DropdownGroup.svelte"; import ArrowRight from "$icon/arrow-right"; import BookMarked from "$icon/book-marked"; import BookBookmark from "$icon/book-bookmark"; import Building2 from "$icon/building-2"; import Download from "$icon/download"; import Cog from "$icon/cog"; import Info from "$icon/info"; 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.ts"; import Separator from "#lib/components/ui/Separator.svelte"; import { siteLinkSections, type SiteLink } from "#lib/components/shell/siteLinks.ts";
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 siteLinkHref = (link: SiteLink) => link.internal ? appPath(link.href) : (link.href as ResolvedPathname);
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 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")}>Sign in</Button>
<Button variant="primary" href={resolve("/signup")} 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 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={BookBookmark} >New repository</DropdownItem > <DropdownItem href={appPath("/repo/new?import=github")} icon={Download} >Import repository</DropdownItem > <Separator /> <DropdownItem href={resolve("/strings/new")} icon={LineSquiggle} >New string</DropdownItem > <Separator /> <DropdownItem href={resolve("/organizations/new")} icon={Building2} >New organization</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="max-md:w-7 max-md:px-0 md:pl-2xs" variant="button" size="small" > {#snippet trigger()} <User did={user.did} handle={user.handle} handleClass="hidden md:inline" loading="eager" /> {/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 data-testid="addAccount" href={appPath(loginHref)}> <span class={style.addBall()}> <Plus class="size-3" aria-hidden="true" /> </span> Add account </DropdownItem> <Separator /> <DropdownItem href={appPath(`/${profileIdentifier(user.handle, user.did)}`)} icon={UserIcon}>Profile</DropdownItem > <DropdownItem href={appPath( `/${profileIdentifier(user.handle, user.did)}?tab=repos` )} icon={BookMarked} > Repositories </DropdownItem> <DropdownItem href={appPath( `/${profileIdentifier(user.handle, user.did)}?tab=strings` )} icon={LineSquiggle} > Strings </DropdownItem> <DropdownItem href={resolve("/settings")} icon={Cog} >Settings</DropdownItem > <DropdownSubmenu label="About" icon={Info}> {#each siteLinkSections as section (section.title)} <DropdownGroup label={section.title} icon={section.icon}> {#each section.links as link (link.href)} <DropdownItem href={siteLinkHref(link)} target={link.newTab ? "_blank" : undefined} rel={link.internal ? undefined : "external noopener noreferrer"} >{link.label}</DropdownItem > {/each} </DropdownGroup> {/each} </DropdownSubmenu> <DropdownItem danger icon={LogOut} onclick={onSignOut} >Sign out</DropdownItem > </Dropdown> {:else} <div class={style.identity({ class: "gap-xs" })}> <Button data-testid="login" variant="ghost" size="small" href={appPath(loginHref)}>Sign in</Button > <Button variant="primary" size="small" href={resolve("/signup")}> Join now </Button> </div> {/if} </div> {/if} </div> </div></nav>