This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web/components: use tv in Topbar

Move the layout classes into a tv slots recipe with marketing/app and
pending variants, replacing the inline ternary and the divider snippet.

Signed-off-by: eti <eti@eti.tf>

+98 -54
+98 -54
web/src/lib/components/shell/Topbar.svelte
··· 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 + 1 45 <script lang="ts"> 2 46 import { resolve } from "$app/paths"; 3 47 import Button from "../ui/Button.svelte"; ··· 21 65 import Separator from "../ui/Separator.svelte"; 22 66 23 67 interface Props { 24 - /** `bare` is the onboarding chrome: the wordmark alone, nothing to click away to */ 25 - variant: "marketing" | "app" | "bare"; 68 + variant: NonNullable<TopbarVariants["variant"]>; 26 69 onSignOut?: () => void | Promise<void>; 27 70 user: CurrentUser | null; 28 71 loading?: boolean; ··· 31 74 let { variant, onSignOut, user, loading = false }: Props = $props(); 32 75 33 76 const appPath = (path: string) => resolve(path as "/"); 77 + 78 + const style = $derived(topbar({ variant })); 34 79 </script> 35 80 36 - {#snippet divider(classes = "")} 37 - <div 38 - class={["hidden h-6 w-px self-center rounded-md bg-background-inset md:block", classes]} 39 - ></div> 40 - {/snippet} 41 - 42 - <nav 43 - class={`h-11 w-full px-5 44 - ${variant === "app" ? "border-b border-border-default bg-background-default shadow-[0_1px_1px_0_rgba(0,0,0,0.05)]" : ""} 45 - `} 46 - aria-label="main" 47 - > 48 - <div class="flex h-full items-center justify-between gap-4"> 49 - <div class="flex shrink-0 items-center gap-2"> 50 - <a 51 - href={resolve("/")} 52 - class="flex shrink-0 items-center gap-2 no-underline hover:no-underline" 53 - > 54 - <span class="hidden sm:inline-flex"> 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()}> 55 86 <Logo wordmark alpha class="h-6.5" /> 56 87 </span> 57 - <span class="inline-flex sm:hidden"> 88 + <span class={style.mark()}> 58 89 <img src="/logos/dolly.svg" alt="tangled" class="size-8" /> 59 90 </span> 60 91 </a> 61 92 </div> 62 93 63 - <div class={variant === "marketing" ? "flex items-center gap-3.5" : "flex items-center gap-4"}> 64 - {#if variant === "bare"} 65 - <!-- nothing: onboarding has no escape hatches in the bar itself --> 66 - {:else if variant === "marketing"} 94 + <div class={style.actions()}> 95 + {#if variant === "marketing"} 67 96 <TopbarSearch /> 68 97 69 - {@render divider()} 98 + <div class={style.divider()}></div> 70 99 71 100 <Button variant="ghost" href={resolve("/login")}>Login</Button> 72 101 ··· 78 107 </Button> 79 108 {:else} 80 109 {#if user || loading} 81 - <div class="order-3 flex items-center md:order-1"> 110 + <div class={style.action({ class: "order-3 hidden sm:block md:order-1" })}> 82 111 <TopbarSearch /> 83 112 </div> 84 113 85 - <div class="order-1 flex items-center md:order-3"> 86 - <Dropdown id="new-menu" group="topbar" variant="button" align="right"> 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 + > 87 123 {#snippet trigger()} 88 124 <Plus class="size-3.5" aria-hidden="true" /> 89 125 <span class="hidden md:inline">New</span> ··· 93 129 </Dropdown> 94 130 </div> 95 131 96 - <div class="order-2 flex items-center md:order-4"> 97 - <a 132 + <div class={style.action({ class: "order-2 md:order-4" })}> 133 + <Button 134 + variant="ghost" 98 135 href={appPath("/notifications")} 99 - class="flex items-center text-foreground-default transition-colors hover:text-foreground-muted" 136 + icon={Bell} 137 + size="sm" 138 + aria-hidden="true" 100 139 aria-label="Notifications" 101 - > 102 - <Bell class="size-4" aria-hidden="true" /> 103 - </a> 140 + /> 104 141 </div> 105 142 106 143 <div class="order-3 md:hidden"> 107 - <a href={appPath("/search")} class="text-foreground-subtle" aria-label="Search"> 108 - <Search class="size-5" aria-hidden="true" /> 109 - </a> 144 + <Button 145 + variant="ghost" 146 + href={appPath("/search")} 147 + icon={Search} 148 + size="sm" 149 + aria-hidden="true" 150 + aria-label="Search" 151 + /> 110 152 </div> 111 153 112 - {@render divider("md:order-2")} 154 + <div class={style.divider({ class: "md:order-2" })}></div> 113 155 {/if} 114 156 115 - <div class="order-4 flex items-center md:order-5"> 157 + <div class={style.action({ class: "order-4 md:order-5" })}> 116 158 {#if loading} 117 - <div class="flex items-center gap-1" role="status" aria-label="Signing in"> 118 - <span 119 - class="flex size-8 items-center justify-center rounded-full border border-border-default bg-background-inset text-foreground-placeholder md:size-5" 120 - > 159 + <div class={style.identity({ pending: true })} role="status" aria-label="Signing in"> 160 + <span class={style.ball({ pending: true })}> 121 161 <Spinner class="size-4" /> 122 162 </span> 123 - <span 124 - class="hidden h-4 w-24 rounded-sm bg-background-inset md:block" 125 - aria-hidden="true" 126 - ></span> 163 + <span class={style.bar()} aria-hidden="true"></span> 127 164 </div> 128 165 {:else if user} 129 - <Dropdown id="profile-menu" group="topbar" align="right"> 166 + <Dropdown 167 + id="profile-menu" 168 + group="topbar" 169 + align="right" 170 + variant="button" 171 + buttonVariant="ghost" 172 + size="sm" 173 + > 130 174 {#snippet trigger()} 131 175 <User did={user.did} handle={user.handle} /> 132 176 {/snippet} ··· 138 182 Strings 139 183 </DropdownItem> 140 184 <DropdownItem href="/settings" icon={Cog}>Settings</DropdownItem> 141 - <Separator/> 185 + <Separator /> 142 186 <DropdownItem danger icon={LogOut} onclick={onSignOut}>Logout</DropdownItem> 143 187 </Dropdown> 144 188 {:else} 145 - <div class="flex items-center gap-2" aria-hidden="true"> 146 - <span class="size-8 rounded-full bg-background-inset md:size-5"></span> 147 - <span class="hidden h-4 w-24 rounded-sm bg-background-inset md:block"></span> 189 + <div class={style.identity()} aria-hidden="true"> 190 + <span class={style.ball()}></span> 191 + <span class={style.bar()}></span> 148 192 </div> 149 193 {/if} 150 194 </div>