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>