/ src / lib / components / header / header.svelte
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>