/ src / components / AppIconRiver.svelte
AppIconRiver.svelte
 1  <script lang="ts">
 2      import { onMount } from 'svelte';
 3      import type { Artwork } from '@jet-app/app-store/api/models';
 4      import AppIcon, { type AppIconProfile } from '~/components/AppIcon.svelte';
 5  
 6      export let icons: Artwork[];
 7      export let profile: AppIconProfile = 'app-icon-river';
 8  
 9      $: aspectRatio = icons[0].width / icons[0].height;
10  
11      let mounted = false;
12      const numberOfIcons = icons.length;
13  
14      // We shift the order of the bottom row of icons to ensure that the same icons aren't shown
15      // next to each other. Note that this is different from purely shuffling the icons, as that
16      // could still lead to the same icons being next to one another, due to how small the set is.
17      // The input and output here is as such:
18      // in  = [1, 2, 3, 4, 5, 6, 7]
19      // out = [4, 5, 6, 7, 1, 2, 3]
20      const iconsInShiftedOrder = [
21          ...icons.slice(numberOfIcons / 2),
22          ...icons.slice(0, numberOfIcons / 2),
23      ];
24  
25      // We are quadrupling the icons we render so the flow is seamless and stretches across the
26      // full width of the container.
27      const topRow = Array(4).fill(icons).flat();
28      const bottomRow = Array(4).fill(iconsInShiftedOrder).flat();
29  
30      // We use this `mounted` flag to defer the rendering of the `AppIconRiver`, since it's markup heavy
31      // and has no semantic meaning for SEO. This deferring saves about 190kb of initial HTML per instance.
32      onMount(() => (mounted = true));
33  </script>
34  
35  {#if mounted}
36      {#each [topRow, bottomRow] as iconRow}
37          <ul class="app-icons">
38              {#each iconRow as icon}
39                  <li
40                      class="app-icon-container"
41                      style:--aspect-ratio={aspectRatio}
42                  >
43                      <AppIcon {icon} {profile} fixedWidth={false} />
44                  </li>
45              {/each}
46          </ul>
47      {/each}
48  {/if}
49  
50  <style lang="scss">
51      @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
52      @use 'ac-sasskit/core/locale' as *;
53  
54      .app-icons {
55          --icon-width: var(--app-icon-river-icon-width, 128px);
56          --speed: var(--app-icon-river-speed, 240s);
57          --direction: -50%;
58  
59          @include rtl {
60              --direction: 50%;
61          }
62          display: flex;
63          width: fit-content;
64          z-index: 2;
65          animation: scroll var(--speed) linear infinite;
66      }
67  
68      .app-icons:last-of-type {
69          margin-bottom: 20px;
70      }
71  
72      .app-icon-container {
73          width: var(--icon-width);
74          aspect-ratio: var(--aspect-ratio);
75          margin: 8px;
76      }
77  
78      .app-icons:last-of-type .app-icon-container {
79          position: relative;
80          right: calc((var(--icon-width) / 2) + 8px);
81      }
82  
83      @keyframes scroll {
84          0% {
85              transform: translateX(0);
86          }
87  
88          100% {
89              transform: translateX(var(--direction));
90          }
91      }
92  </style>