DefaultPage.svelte
1 <script lang="ts" context="module"> 2 import type { 3 PagePresentationOptions, 4 Shelf, 5 } from '@jet-app/app-store/api/models'; 6 import type { WebRenderablePage } from '@jet-app/app-store/api/models/web-renderable-page'; 7 8 /** 9 * Just the `Page` props that we actually need to render this component 10 */ 11 export interface DefaultPageRequirements extends WebRenderablePage { 12 shelves: Shelf[]; 13 presentationOptions?: PagePresentationOptions; 14 } 15 </script> 16 17 <script lang="ts"> 18 import type { MarkerShelf } from '~/components/jet/shelf/MarkerShelf.svelte'; 19 import { isUberShelf } from '~/components/jet/shelf/UberShelf.svelte'; 20 import ShelfComponent from '~/components/jet/shelf/Shelf.svelte'; 21 import { partition } from '~/utils/array'; 22 import { carouselMediaStyle } from '~/stores/carousel-media-style'; 23 import mediaQueries from '~/utils/media-queries'; 24 import { isHeroCarouselShelf } from '../jet/shelf/HeroCarouselShelf.svelte'; 25 import { isRtl } from '~/utils/locale'; 26 27 interface $$Slots { 28 'before-shelves': {}; 29 30 /** 31 * If {@linkcode ShelfComponent}` recognizes a shelf to be a {@linkcode MarkerShelf}, 32 * this slot will be rendered so that the "page" data can be supplied by a "parent" 33 * component 34 */ 35 'marker-shelf': { 36 shelf: MarkerShelf; 37 }; 38 } 39 40 export let page: DefaultPageRequirements; 41 42 $: ({ title, presentationOptions = [] } = page); 43 44 // Some shelves are meant to be rendered above the title, rather than below it 45 $: [aboveTitleShelves, belowTitleShelves] = partition( 46 page.shelves, 47 (shelf) => { 48 // Some "uber" shelves might be placed above the title 49 if (isUberShelf(shelf)) { 50 const [uber] = shelf.items; 51 return uber.style === 'above'; 52 } 53 54 // Everything else should be below it 55 return false; 56 }, 57 ); 58 59 $: prefersHiddenPageTitle = presentationOptions.includes( 60 'prefersHiddenPageTitle', 61 ); 62 $: prefersLargeTitle = presentationOptions.includes('prefersLargeTitle'); 63 $: prefersOverlayedPageHeader = 64 $mediaQueries === 'xsmall' && 65 presentationOptions.includes('prefersOverlayedPageHeader'); 66 $: isOnDarkBackground = 67 prefersOverlayedPageHeader && $carouselMediaStyle === 'dark'; 68 69 $: isTitleDuplicatedInHero = (() => { 70 const firstShelf = page.shelves?.[0]; 71 72 if ( 73 !firstShelf || 74 !isHeroCarouselShelf(firstShelf) || 75 firstShelf.items?.length !== 1 76 ) { 77 return false; 78 } 79 80 const { items: ltrItems, rtlItems } = firstShelf.items?.[0] ?? {}; 81 const firstItem = isRtl() && rtlItems?.length ? rtlItems : ltrItems; 82 const firstTitle = firstItem?.[0]?.overlay?.titleText; 83 84 return title === firstTitle; 85 })(); 86 </script> 87 88 <div 89 class="default-page-container" 90 data-testid="default-page-container" 91 class:with-overlaid-title={prefersOverlayedPageHeader} 92 class:with-title-in-hero={isTitleDuplicatedInHero} 93 > 94 {#each aboveTitleShelves as shelf} 95 <ShelfComponent {shelf}> 96 <slot name="marker-shelf" slot="marker-shelf" let:shelf {shelf} /> 97 </ShelfComponent> 98 {/each} 99 100 {#if title && !prefersHiddenPageTitle && !isTitleDuplicatedInHero} 101 <h1 102 data-test-id="page-title" 103 class:large-title={prefersLargeTitle} 104 class:overlaid={prefersOverlayedPageHeader} 105 class:on-dark-background={isOnDarkBackground} 106 > 107 {title} 108 </h1> 109 {/if} 110 111 <slot name="before-shelves" /> 112 113 {#each belowTitleShelves as shelf} 114 {#if !shelf.isHidden} 115 <ShelfComponent {shelf}> 116 <slot 117 name="marker-shelf" 118 slot="marker-shelf" 119 let:shelf 120 {shelf} 121 /> 122 </ShelfComponent> 123 {/if} 124 {/each} 125 </div> 126 127 <style lang="scss"> 128 @use 'ac-sasskit/modules/viewportcontent/core' as *; 129 @use 'amp/stylekit/core/viewports' as *; 130 131 .default-page-container { 132 flex-grow: 1; 133 width: 100%; 134 max-width: viewport-content-for(xlarge); 135 margin: 0 auto; 136 } 137 138 .default-page-container.with-overlaid-title { 139 margin-top: -13px; 140 } 141 142 .default-page-container.with-title-in-hero { 143 @media (--range-small-up) { 144 margin-top: 10px; 145 } 146 } 147 148 h1 { 149 padding: 11px var(--bodyGutter); 150 font: var(--large-title-emphasized); 151 letter-spacing: -0.5px; 152 word-wrap: break-word; 153 color: var(--systemPrimary, #000); 154 position: relative; 155 z-index: 1; 156 transition: color 210ms ease-in; 157 } 158 159 h1.large-title { 160 font: var(--large-title-emphasized-tall); 161 } 162 163 h1.overlaid { 164 position: absolute; 165 z-index: 3; 166 padding: var(--bodyGutter) var(--bodyGutter) 0; 167 color: var(--systemPrimary-onLight, #000); 168 } 169 170 h1.on-dark-background { 171 color: var(--systemPrimary-onDark); 172 } 173 </style>