/ src / components / hero / Carousel.svelte
Carousel.svelte
  1  <!--
  2  @component
  3  Component for rendering a carousel of `Hero.svelte` components in a way taht's decoupled from
  4  any particular data model
  5  -->
  6  <script lang="ts" generics="Item">
  7      import type { Opt } from '@jet/environment/types/optional';
  8      import type { Artwork, Shelf } from '@jet-app/app-store/api/models';
  9  
 10      import HorizontalShelf from '~/components/jet/shelf/HorizontalShelf.svelte';
 11      import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
 12      import { intersectionObserver } from '@amp/web-app-components/src/actions/intersection-observer';
 13      import mediaQueries from '~/utils/media-queries';
 14      import { sidebarIsHidden } from '@amp/web-app-components/src/stores/sidebar-hidden';
 15      import HeroCarouselBackgroundPortal, {
 16          id as portalId,
 17      } from '~/components/hero/CarouselBackgroundPortal.svelte';
 18      import AmbientBackgroundArtwork from '~/components/AmbientBackgroundArtwork.svelte';
 19      import portal from '~/utils/portal';
 20      import { carouselMediaStyle } from '~/stores/carousel-media-style';
 21  
 22      interface $$Slots {
 23          default: {
 24              /**
 25               * The `Item` to render as a `Hero` in the carousel
 26               */
 27              item: Item;
 28          };
 29      }
 30  
 31      /**
 32       * The shelf being rendered
 33       *
 34       * Used to derrive any shelf-specific presentation
 35       */
 36      export let shelf: Shelf;
 37  
 38      /**
 39       * The items to render in the hero carousel
 40       *
 41       * This is decoupled from `shelf` to avoid assuming that `shelf.items` is, itself,
 42       * the set of items that we need to present; some shelves model their items as chilren
 43       * of the first shelf item.
 44       */
 45      export let items: Item[];
 46  
 47      /**
 48       * Callback that determines the "background artwork" to use behind the
 49       * active `Hero` for the given `Item`
 50       */
 51      export let deriveBackgroundArtworkFromItem: (item: Item) => Opt<Artwork>;
 52  
 53      $: gridRows = shelf.rowsPerColumn ?? undefined;
 54      $: isXSmallViewport = $mediaQueries === 'xsmall';
 55  
 56      let activeIndex: number | undefined = 0;
 57  
 58      function createIntersectionObserverCallback(index: number) {
 59          return (isIntersectingViewport: boolean) => {
 60              if (isIntersectingViewport) {
 61                  // Many different types of `item`s can be rendered in this Carousel, and all those
 62                  // different items have different ways of determining whether or not the background
 63                  // is dark or light, so we are running through all the options here.
 64                  const { style, mediaOverlayStyle, isMediaDark } = items[
 65                      index
 66                  ] as any;
 67                  const fallbackStyle = 'dark';
 68                  let derivedStyle;
 69  
 70                  if (typeof isMediaDark !== 'undefined') {
 71                      derivedStyle = isMediaDark ? 'dark' : 'light';
 72                  }
 73  
 74                  carouselMediaStyle.set(
 75                      style || mediaOverlayStyle || derivedStyle || fallbackStyle,
 76                  );
 77  
 78                  activeIndex = index;
 79              }
 80          };
 81      }
 82  </script>
 83  
 84  <HeroCarouselBackgroundPortal />
 85  
 86  <ShelfWrapper {shelf} --shelfGridGutterWidth="0">
 87      <HorizontalShelf
 88          {gridRows}
 89          {items}
 90          --shelfScrollPaddingInline="0"
 91          --grid-max-content-xsmall={!$sidebarIsHidden
 92              ? 'calc(100% + 50px)'
 93              : '100vw'}
 94          gridType="Spotlight"
 95          let:item
 96          let:index
 97      >
 98          {#if isXSmallViewport}
 99              <div
100                  use:intersectionObserver={{
101                      callback: createIntersectionObserverCallback(index),
102                      threshold: 0.5,
103                  }}
104              >
105                  <slot {item} />
106              </div>
107          {:else}
108              <div
109                  use:intersectionObserver={{
110                      callback: createIntersectionObserverCallback(index),
111                      threshold: 0,
112                  }}
113              >
114                  {#if !import.meta.env.SSR}
115                      {@const backgroundArtwork =
116                          deriveBackgroundArtworkFromItem(item)}
117  
118                      {#if backgroundArtwork}
119                          <div use:portal={portalId}>
120                              <AmbientBackgroundArtwork
121                                  artwork={backgroundArtwork}
122                                  active={activeIndex === index}
123                              />
124                          </div>
125                      {/if}
126                  {/if}
127  
128                  <slot {item} />
129              </div>
130          {/if}
131      </HorizontalShelf>
132  </ShelfWrapper>