header.svelte
1 <script lang="ts" module> 2 import { gql } from 'graphql-request'; 3 import { COLLECT_BUTTON_WITHDRAWABLE_BALANCE_FRAGMENT } from '../collect-button/collect-button.svelte'; 4 5 export const HEADER_USER_FRAGMENT = gql` 6 ${COLLECT_BUTTON_WITHDRAWABLE_BALANCE_FRAGMENT} 7 fragment HeaderUser on User { 8 chainData { 9 chain 10 withdrawableBalances { 11 ...CollectButtonWithdrawableBalance 12 } 13 } 14 } 15 `; 16 17 export const hideElevation = writable(false); 18 </script> 19 20 <script lang="ts"> 21 import scroll from '$lib/stores/scroll'; 22 import ConnectButton from '../connect-button/connect-button.svelte'; 23 import SearchBar from '../search-bar/search-bar.svelte'; 24 import DripsLogo from '$lib/components/illustrations/logo.svelte'; 25 import SearchIcon from '$lib/components/icons/MagnifyingGlass.svelte'; 26 import { fade, fly } from 'svelte/transition'; 27 import { quadInOut } from 'svelte/easing'; 28 import Spinner from '../spinner/spinner.svelte'; 29 import CollectButton from '../collect-button/collect-button.svelte'; 30 import breakpointsStore from '$lib/stores/breakpoints/breakpoints.store'; 31 import type { HeaderUserFragment } from './__generated__/gql.generated'; 32 import walletStore from '$lib/stores/wallet/wallet.store'; 33 import Flyout from '../flyout/flyout.svelte'; 34 import NetworkPicker from '../network-picker/network-picker.svelte'; 35 import NetworkList from '../network-picker/components/network-list.svelte'; 36 import cupertinoPaneStore from '$lib/stores/cupertino-pane/cupertino-pane.store'; 37 import filterCurrentChainData from '$lib/utils/filter-current-chain-data'; 38 import network from '$lib/stores/wallet/network'; 39 import wallet from '$lib/stores/wallet/wallet.store'; 40 import { writable } from 'svelte/store'; 41 import ReadOnlyBanner from '../read-only-banner/read-only-banner.svelte'; 42 43 interface Props { 44 user: HeaderUserFragment | null; 45 showLoadingIndicator?: boolean; 46 } 47 48 let { user, showLoadingIndicator = false }: Props = $props(); 49 50 let searchMode = $state(false); 51 52 let collectButtonPeeking = $state(false); 53 54 let networkPickerExpanded = $state(false); 55 let chainData = $derived(user?.chainData ? filterCurrentChainData(user.chainData) : undefined); 56 let elevated = $derived(!$hideElevation && $scroll.pos > 16); 57 let connected = $derived($walletStore.connected); 58 let safeAppMode = $derived(Boolean($wallet.safe)); 59 </script> 60 61 <div class="header-wrapper"> 62 <ReadOnlyBanner /> 63 <header class:elevated class:search-mode={searchMode}> 64 {#if !connected || $breakpointsStore?.breakpoint === 'desktop' || $breakpointsStore?.breakpoint === 'desktopWide'} 65 <a aria-label="Go to explore page" href="/app"> 66 <div class="logo flex items-center pb-px"> 67 <DripsLogo /> 68 </div> 69 70 <div class="loading-indicator" class:loading={showLoadingIndicator}> 71 <Spinner /> 72 </div> 73 </a> 74 {/if} 75 {#if connected && ($breakpointsStore?.breakpoint === 'mobile' || $breakpointsStore?.breakpoint === 'tablet')} 76 <div data-highlightid="global-collect" class="collect mobile"> 77 <CollectButton 78 withdrawableBalances={chainData?.withdrawableBalances} 79 peekAmount={true} 80 bind:isPeeking={collectButtonPeeking} 81 /> 82 </div> 83 <div></div> 84 {:else} 85 <!-- ensure nav items are right-aligned on mobile still even though nothing's on the left --> 86 <div></div> 87 {/if} 88 <div class="search-bar"> 89 <SearchBar bind:searchOpen={searchMode} /> 90 </div> 91 <div class="right" class:collect-button-peeking={collectButtonPeeking}> 92 <div class="header-buttons"> 93 {#if !searchMode} 94 <button 95 class="header-button" 96 onclick={() => (searchMode = true)} 97 transition:fly={{ duration: 300, x: -64, easing: quadInOut }} 98 data-testid="search-button" 99 data-highlightid="search" 100 > 101 <SearchIcon style="fill: var(--color-foreground)" /> 102 </button> 103 {/if} 104 105 {#if network.displayNetworkPicker && !safeAppMode} 106 <div class="network-picker"> 107 <div class="desktop-only"> 108 <div class="network-picker-flyout"> 109 <Flyout width="16rem" bind:visible={networkPickerExpanded}> 110 {#snippet trigger()} 111 <div> 112 <NetworkPicker 113 toggled={networkPickerExpanded} 114 onclick={() => (networkPickerExpanded = !networkPickerExpanded)} 115 /> 116 </div> 117 {/snippet} 118 {#snippet content()} 119 <div> 120 <NetworkList /> 121 </div> 122 {/snippet} 123 </Flyout> 124 </div> 125 </div> 126 127 <div 128 class="mobile-only" 129 role="button" 130 tabindex="0" 131 onclick={() => cupertinoPaneStore.openSheet(NetworkList, undefined)} 132 onkeydown={() => cupertinoPaneStore.openSheet(NetworkList, undefined)} 133 > 134 <NetworkPicker /> 135 </div> 136 </div> 137 {/if} 138 </div> 139 <div class="connect"> 140 <ConnectButton /> 141 </div> 142 {#if $walletStore.connected && ($breakpointsStore?.breakpoint === 'desktop' || $breakpointsStore?.breakpoint === 'desktopWide')} 143 <div data-highlightid="global-collect" class="collect"> 144 <CollectButton withdrawableBalances={chainData?.withdrawableBalances} /> 145 </div> 146 {/if} 147 </div> 148 149 {#if searchMode} 150 <!-- svelte-ignore a11y_click_events_have_key_events --> 151 <!-- svelte-ignore a11y_no_static_element_interactions --> 152 <div 153 class="search-background" 154 transition:fade={{ duration: 300 }} 155 onclick={() => (searchMode = false)} 156 ></div> 157 {/if} 158 </header> 159 </div> 160 161 <style> 162 .header-wrapper { 163 display: flex; 164 flex-direction: column; 165 } 166 167 header { 168 height: 4rem; 169 width: 100%; 170 background-color: var(--color-background); 171 transition: 172 box-shadow 0.3s, 173 background-color 0.5s; 174 display: flex; 175 align-items: center; 176 justify-content: space-between; 177 padding: 1rem 1.5rem; 178 gap: 0.5rem; 179 view-transition-name: header; 180 } 181 182 :root::view-transition-group(header) { 183 z-index: 10; 184 } 185 186 .loading-indicator { 187 visibility: hidden; 188 opacity: 0; 189 transition: opacity 0.3s; 190 } 191 192 .loading-indicator.loading { 193 visibility: visible; 194 opacity: 1; 195 } 196 197 .logo { 198 height: 1.5rem; 199 } 200 201 a { 202 display: flex; 203 gap: 0.5rem; 204 align-items: center; 205 } 206 207 header.elevated { 208 box-shadow: var(--elevation-low); 209 } 210 211 .right { 212 display: flex; 213 gap: 0.5rem; 214 align-items: center; 215 transition: opacity 0.3s; 216 } 217 218 .right.collect-button-peeking { 219 opacity: 0.5; 220 } 221 222 .header-buttons { 223 display: flex; 224 } 225 226 .header-buttons > .header-button { 227 border-radius: 1.5rem; 228 height: 2.5rem; 229 width: 2.5rem; 230 display: flex; 231 justify-content: center; 232 align-items: center; 233 transition: 234 background-color 0.3s, 235 box-shadow 0.3s; 236 cursor: pointer; 237 } 238 239 .header-buttons > .header-button:hover { 240 background-color: var(--color-foreground-level-1); 241 } 242 243 .header-buttons > .header-button:focus-visible { 244 background-color: var(--color-foreground-level-1); 245 box-shadow: var(--elevation-low); 246 outline: none; 247 } 248 249 .search-bar { 250 position: fixed; 251 top: 0.5rem; 252 left: 50%; 253 transform: translateX(-50%); 254 width: 100%; 255 max-width: 32rem; 256 z-index: 100; 257 } 258 259 .search-background { 260 position: fixed; 261 top: 0; 262 left: 0; 263 right: 0; 264 bottom: 0; 265 height: 100vh; 266 background-color: var(--color-background); 267 opacity: 0.9; 268 z-index: 50; 269 } 270 271 @media (max-width: 577px) { 272 header { 273 padding: 1rem; 274 } 275 276 .collect.mobile { 277 position: absolute; 278 z-index: 10; 279 } 280 } 281 282 .network-picker-flyout { 283 display: flex; 284 align-items: center; 285 margin-left: 0.5rem; 286 } 287 288 .network-picker { 289 display: flex; 290 align-items: center; 291 justify-content: center; 292 gap: 0.5rem; 293 } 294 295 .mobile-only { 296 display: none; 297 } 298 299 @media (max-width: 768px) { 300 .desktop-only { 301 display: none !important; 302 } 303 304 .mobile-only { 305 display: initial; 306 } 307 } 308 </style>