summaryrefslogtreecommitdiff
path: root/src/components/jet/shelf
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/jet/shelf')
-rw-r--r--src/components/jet/shelf/AccessibilityDeveloperLinkShelf.svelte36
-rw-r--r--src/components/jet/shelf/AccessibilityFeaturesShelf.svelte35
-rw-r--r--src/components/jet/shelf/AccessibilityHeaderShelf.svelte182
-rw-r--r--src/components/jet/shelf/ActionShelf.svelte80
-rw-r--r--src/components/jet/shelf/AnnotationShelf.svelte49
-rw-r--r--src/components/jet/shelf/AppEventDetailShelf.svelte290
-rw-r--r--src/components/jet/shelf/AppPromotionShelf.svelte47
-rw-r--r--src/components/jet/shelf/AppShowcaseShelf.svelte29
-rw-r--r--src/components/jet/shelf/AppTrailerLockupShelf.svelte48
-rw-r--r--src/components/jet/shelf/ArcadeFooterShelf.svelte32
-rw-r--r--src/components/jet/shelf/BannerShelf.svelte35
-rw-r--r--src/components/jet/shelf/BrickShelf.svelte31
-rw-r--r--src/components/jet/shelf/CategoryBrickShelf.svelte28
-rw-r--r--src/components/jet/shelf/EditorialCardShelf.svelte32
-rw-r--r--src/components/jet/shelf/EditorialLinkShelf.svelte122
-rw-r--r--src/components/jet/shelf/FallbackShelf.svelte39
-rw-r--r--src/components/jet/shelf/FramedArtworkShelf.svelte98
-rw-r--r--src/components/jet/shelf/FramedVideoShelf.svelte78
-rw-r--r--src/components/jet/shelf/HeroCarouselShelf.svelte38
-rw-r--r--src/components/jet/shelf/HorizontalRuleShelf.svelte54
-rw-r--r--src/components/jet/shelf/HorizontalShelf.svelte53
-rw-r--r--src/components/jet/shelf/InAppPurchaseLockupShelf.svelte31
-rw-r--r--src/components/jet/shelf/LargeBrickShelf.svelte26
-rw-r--r--src/components/jet/shelf/LargeHeroBreakoutShelf.svelte31
-rw-r--r--src/components/jet/shelf/LargeImageLockupShelf.svelte30
-rw-r--r--src/components/jet/shelf/LargeLockupShelf.svelte28
-rw-r--r--src/components/jet/shelf/LargeStoryCardShelf.svelte32
-rw-r--r--src/components/jet/shelf/LinkableTextShelf.svelte43
-rw-r--r--src/components/jet/shelf/MarkerShelf.svelte36
-rw-r--r--src/components/jet/shelf/MediumImageLockupShelf.svelte28
-rw-r--r--src/components/jet/shelf/MediumLockupShelf.svelte31
-rw-r--r--src/components/jet/shelf/MediumStoryCardShelf.svelte31
-rw-r--r--src/components/jet/shelf/PageHeaderShelf.svelte34
-rw-r--r--src/components/jet/shelf/ParagraphShelf.svelte52
-rw-r--r--src/components/jet/shelf/PosterLockupShelf.svelte31
-rw-r--r--src/components/jet/shelf/PrivacyFooterShelf.svelte40
-rw-r--r--src/components/jet/shelf/PrivacyHeaderShelf.svelte145
-rw-r--r--src/components/jet/shelf/PrivacyTypeShelf.svelte29
-rw-r--r--src/components/jet/shelf/ProductBadgeShelf.svelte59
-rw-r--r--src/components/jet/shelf/ProductCapabilityShelf.svelte31
-rw-r--r--src/components/jet/shelf/ProductDescriptionShelf.svelte95
-rw-r--r--src/components/jet/shelf/ProductMediaShelf.svelte269
-rw-r--r--src/components/jet/shelf/ProductPageLinkShelf.svelte59
-rw-r--r--src/components/jet/shelf/ProductRatingsShelf.svelte29
-rw-r--r--src/components/jet/shelf/ProductReviewShelf.svelte38
-rw-r--r--src/components/jet/shelf/QuoteShelf.svelte80
-rw-r--r--src/components/jet/shelf/ReviewsContainerShelf.svelte84
-rw-r--r--src/components/jet/shelf/ReviewsShelf.svelte28
-rw-r--r--src/components/jet/shelf/RibbonBarShelf.svelte135
-rw-r--r--src/components/jet/shelf/SearchLinkShelf.svelte26
-rw-r--r--src/components/jet/shelf/SearchResultShelf.svelte49
-rw-r--r--src/components/jet/shelf/Shelf.svelte320
-rw-r--r--src/components/jet/shelf/SmallBreakoutShelf.svelte32
-rw-r--r--src/components/jet/shelf/SmallBrickShelf.svelte26
-rw-r--r--src/components/jet/shelf/SmallLockupShelf.svelte54
-rw-r--r--src/components/jet/shelf/SmallStoryCardShelf.svelte66
-rw-r--r--src/components/jet/shelf/TitledParagraphShelf.svelte118
-rw-r--r--src/components/jet/shelf/TodayCardShelf.svelte187
-rw-r--r--src/components/jet/shelf/UberShelf.svelte40
59 files changed, 3939 insertions, 0 deletions
diff --git a/src/components/jet/shelf/AccessibilityDeveloperLinkShelf.svelte b/src/components/jet/shelf/AccessibilityDeveloperLinkShelf.svelte
new file mode 100644
index 0000000..c1e7b2e
--- /dev/null
+++ b/src/components/jet/shelf/AccessibilityDeveloperLinkShelf.svelte
@@ -0,0 +1,36 @@
+<script lang="ts" context="module">
+ import type {
+ AccessibilityParagraph,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ interface AccessibilityDeveloperLinkShelf extends Shelf {
+ items: [AccessibilityParagraph];
+ }
+
+ export function isAccessibilityDeveloperLinkShelf(
+ shelf: Shelf,
+ ): shelf is AccessibilityDeveloperLinkShelf {
+ let { contentType, items, title } = shelf;
+
+ return (
+ contentType === 'accessibilityParagraph' &&
+ !title &&
+ Array.isArray(items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import AccessibilityParagraphItem from '../item/AccessibilityParagraphItem.svelte';
+ import { getAccessibilityLayoutConfiguration } from '~/context/accessibility-layout';
+
+ export let shelf: AccessibilityDeveloperLinkShelf;
+
+ $: ({ withBottomPadding } = getAccessibilityLayoutConfiguration(shelf));
+</script>
+
+<ShelfWrapper {shelf} centered {withBottomPadding}>
+ <AccessibilityParagraphItem item={shelf.items[0]} />
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/AccessibilityFeaturesShelf.svelte b/src/components/jet/shelf/AccessibilityFeaturesShelf.svelte
new file mode 100644
index 0000000..cb2fed8
--- /dev/null
+++ b/src/components/jet/shelf/AccessibilityFeaturesShelf.svelte
@@ -0,0 +1,35 @@
+<script lang="ts" context="module">
+ import type {
+ AccessibilityFeatures,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ export interface AccessibilityFeaturesShelf extends Shelf {
+ items: AccessibilityFeatures[];
+ }
+
+ export function isAccessibilityFeaturesShelf(
+ shelf: Shelf,
+ ): shelf is AccessibilityFeaturesShelf {
+ let { contentType, items } = shelf;
+
+ return contentType === 'accessibilityFeatures' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import AccessibilityFeaturesItem from '~/components/jet/item/AccessibilityFeaturesItem.svelte';
+ import { getAccessibilityLayoutConfiguration } from '~/context/accessibility-layout';
+
+ export let shelf: AccessibilityFeaturesShelf;
+
+ $: ({ withBottomPadding } = getAccessibilityLayoutConfiguration(shelf));
+</script>
+
+<ShelfWrapper {shelf} {withBottomPadding}>
+ <ShelfItemLayout {shelf} gridType="B" let:item>
+ <AccessibilityFeaturesItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/AccessibilityHeaderShelf.svelte b/src/components/jet/shelf/AccessibilityHeaderShelf.svelte
new file mode 100644
index 0000000..990c507
--- /dev/null
+++ b/src/components/jet/shelf/AccessibilityHeaderShelf.svelte
@@ -0,0 +1,182 @@
+<script lang="ts" context="module">
+ import {
+ type Action,
+ type FlowAction,
+ type GenericPage,
+ type AccessibilityParagraph,
+ type Shelf,
+ isFlowAction,
+ } from '@jet-app/app-store/api/models';
+
+ import {
+ isAccessibilityFeaturesShelf,
+ type AccessibilityFeaturesShelf,
+ } from '~/components/jet/shelf/AccessibilityFeaturesShelf.svelte';
+
+ interface AccessibilityParagraphShelf extends Shelf {
+ items: [AccessibilityParagraph];
+ }
+
+ interface AccessibilityHeaderShelf extends AccessibilityParagraphShelf {
+ items: [AccessibilityParagraph];
+ }
+
+ interface AccessibilityDetailPage extends GenericPage {
+ shelves: (AccessibilityFeaturesShelf | AccessibilityParagraphShelf)[];
+ }
+
+ interface AccessibilityDetailPageFlowAction extends FlowAction {
+ page: 'accessibilityDetails';
+ pageData: AccessibilityDetailPage;
+ }
+
+ export function isAccessibilityHeaderShelf(
+ shelf: Shelf,
+ ): shelf is AccessibilityHeaderShelf {
+ let { contentType, items, title } = shelf;
+
+ return (
+ contentType === 'accessibilityParagraph' &&
+ !!title &&
+ Array.isArray(items)
+ );
+ }
+
+ function isAccessibilityParagraphShelf(
+ shelf: Shelf,
+ ): shelf is AccessibilityParagraphShelf {
+ let { contentType, items } = shelf;
+
+ return contentType === 'accessibilityParagraph' && Array.isArray(items);
+ }
+
+ function isAccessibilityDetailFlowAction(
+ action: Action,
+ ): action is AccessibilityDetailPageFlowAction {
+ return isFlowAction(action) && action.page === 'accessibilityDetails';
+ }
+</script>
+
+<script lang="ts">
+ import Modal from '@amp/web-app-components/src/components/Modal/Modal.svelte';
+ import ContentModal from '~/components/jet/item/ContentModal.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfTitle from '~/components/Shelf/Title.svelte';
+ import AccessibilityParagraphItem from '~/components/jet/item/AccessibilityParagraphItem.svelte';
+ import AccessibilityFeaturesItem from '~/components/jet/item/AccessibilityFeaturesItem.svelte';
+ import { getI18n } from '~/stores/i18n';
+ import { getAccessibilityLayoutConfiguration } from '~/context/accessibility-layout';
+
+ export let shelf: AccessibilityHeaderShelf;
+
+ $: ({ withBottomPadding } = getAccessibilityLayoutConfiguration(shelf));
+
+ let modalComponent: Modal | undefined;
+ let modalTriggerElement: HTMLElement | null = null;
+
+ const { seeAllAction } = shelf;
+ const i18n = getI18n();
+ const translateFn = (key: string) => $i18n.t(key);
+ const handleModalClose = () => modalComponent?.close();
+ const handleOpenModalClick = (e: Event) => {
+ modalTriggerElement = e.target as HTMLElement;
+ modalComponent?.showModal();
+ };
+
+ const destination =
+ seeAllAction && isAccessibilityDetailFlowAction(seeAllAction)
+ ? seeAllAction
+ : undefined;
+ const pageData = destination?.pageData;
+</script>
+
+<ShelfWrapper {shelf} {withBottomPadding}>
+ <div slot="title" class="title-container">
+ {#if shelf.title}
+ {#if destination}
+ <button on:click={handleOpenModalClick}>
+ <ShelfTitle
+ title={shelf.title}
+ seeAllAction={destination}
+ />
+ </button>
+ {:else}
+ <ShelfTitle title={shelf.title} />
+ {/if}
+ {/if}
+
+ {#if pageData}
+ <Modal {modalTriggerElement} bind:this={modalComponent}>
+ <ContentModal
+ on:close={handleModalClose}
+ title={pageData.title || null}
+ subtitle={null}
+ >
+ <svelte:fragment slot="content">
+ <div class="modal-content-container">
+ {#each pageData.shelves as shelf}
+ <div class="content-section">
+ {#if isAccessibilityParagraphShelf(shelf)}
+ {#each shelf.items as item}
+ <AccessibilityParagraphItem
+ {item}
+ />
+ {/each}
+ {/if}
+
+ {#if isAccessibilityFeaturesShelf(shelf)}
+ {#each shelf.items as item}
+ <AccessibilityFeaturesItem
+ {item}
+ isDetailView={true}
+ />
+ {/each}
+ {/if}
+ </div>
+ {/each}
+ </div>
+ </svelte:fragment>
+ </ContentModal>
+ </Modal>
+ {/if}
+ </div>
+
+ <div class="header-container">
+ <div>
+ <AccessibilityParagraphItem item={shelf.items[0]} />
+ </div>
+ </div>
+</ShelfWrapper>
+
+<style>
+ .title-container {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 16px;
+ padding-inline-end: var(--bodyGutter);
+ }
+
+ .header-container {
+ margin: 0 var(--bodyGutter);
+ }
+
+ .header-container div {
+ @media (--range-medium-up) {
+ width: 66%;
+ }
+ }
+
+ .modal-content-container {
+ font: var(--body-tall);
+ white-space: normal;
+ }
+
+ .modal-content-container .content-section {
+ padding-top: 20px;
+ border-top: 1px solid var(--defaultLine);
+ }
+
+ .modal-content-container .content-section:not(:first-child) {
+ margin-top: 20px;
+ }
+</style>
diff --git a/src/components/jet/shelf/ActionShelf.svelte b/src/components/jet/shelf/ActionShelf.svelte
new file mode 100644
index 0000000..847438f
--- /dev/null
+++ b/src/components/jet/shelf/ActionShelf.svelte
@@ -0,0 +1,80 @@
+<script lang="ts" context="module">
+ import type { Shelf, Action } from '@jet-app/app-store/api/models';
+
+ interface ActionShelf extends Shelf {
+ items: Action[];
+ }
+
+ export function isActionShelf(shelf: Shelf): shelf is ActionShelf {
+ return shelf.contentType === 'action' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import Artwork, { getNaturalProfile } from '~/components/Artwork.svelte';
+ import LinkWrapper from '~/components/LinkWrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: ActionShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="F" let:item>
+ {@const action = item}
+ {@const artwork = item.artwork}
+ {@const title = item.title}
+
+ <div class="container">
+ <LinkWrapper {action}>
+ {#if artwork}
+ <div class="artwork-container" aria-hidden="true">
+ <Artwork
+ {artwork}
+ profile={getNaturalProfile(artwork, [24])}
+ hasTransparentBackground
+ />
+ </div>
+ {/if}
+ {title}
+ </LinkWrapper>
+ </div>
+ </ShelfItemLayout>
+</ShelfWrapper>
+
+<style>
+ .container :global(a) {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ background: var(--pageBG);
+ border-radius: var(--global-border-radius-medium);
+ box-shadow: var(--shadow-small);
+ padding: 16px 10px;
+ width: 100%;
+ font: var(--title-3-medium);
+ transition: background-color 210ms ease-out;
+ }
+
+ .container :global(a:hover) {
+ /* stylelint-disable color-function-notation */
+ background-color: rgb(from var(--pageBG) r g b/0.1);
+ /* stylelint-enable color-function-notation */
+
+ @media (prefers-color-scheme: dark) {
+ /* stylelint-disable color-function-notation */
+ background-color: rgb(from var(--pageBG) r g b/0.85);
+ /* stylelint-enable color-function-notation */
+ }
+ }
+
+ .artwork-container {
+ width: 24px;
+ height: 24px;
+ }
+
+ .container :global(.external-link-arrow) {
+ height: 10px;
+ }
+</style>
diff --git a/src/components/jet/shelf/AnnotationShelf.svelte b/src/components/jet/shelf/AnnotationShelf.svelte
new file mode 100644
index 0000000..e11de72
--- /dev/null
+++ b/src/components/jet/shelf/AnnotationShelf.svelte
@@ -0,0 +1,49 @@
+<script lang="ts" context="module">
+ import type { Shelf, Annotation } from '@jet-app/app-store/api/models';
+
+ interface AnnotationShelf extends Shelf {
+ items: Annotation[];
+ }
+
+ export function isAnnotationShelf(shelf: Shelf): shelf is AnnotationShelf {
+ const { contentType, items } = shelf;
+
+ return contentType === 'annotation' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import Grid from '~/components/Grid.svelte';
+ import CollapsableContent from '~/components/CollapsableContent.svelte';
+ import AnnotationItem from '~/components/jet/item/Annotation/AnnotationItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: AnnotationShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <dl>
+ <Grid items={shelf.items} gridType="F" let:item>
+ <dt>{item.title}</dt>
+
+ {#if item.summary}
+ <CollapsableContent>
+ <svelte:fragment slot="summary">
+ {item.summary}
+ </svelte:fragment>
+
+ <AnnotationItem {item} />
+ </CollapsableContent>
+ {:else}
+ <AnnotationItem {item} />
+ {/if}
+ </Grid>
+ </dl>
+</ShelfWrapper>
+
+<style>
+ dt {
+ color: var(--systemSecondary);
+ margin-bottom: 4px;
+ }
+</style>
diff --git a/src/components/jet/shelf/AppEventDetailShelf.svelte b/src/components/jet/shelf/AppEventDetailShelf.svelte
new file mode 100644
index 0000000..2ae84eb
--- /dev/null
+++ b/src/components/jet/shelf/AppEventDetailShelf.svelte
@@ -0,0 +1,290 @@
+<script lang="ts" context="module">
+ import {
+ type AppEventDetailShelf,
+ isAppEventDetailShelf,
+ isFlowAction,
+ } from '@jet-app/app-store/api/models';
+
+ export { isAppEventDetailShelf };
+</script>
+
+<script lang="ts">
+ import type { CropCode } from '@amp/web-app-components/src/components/Artwork/types';
+ import { buildSrc } from '@amp/web-app-components/src/components/Artwork/utils/srcset';
+ import mediaQueries from '~/utils/media-queries';
+ import Artwork from '~/components/Artwork.svelte';
+ import Video from '~/components/jet/Video.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import SmallLockupItem from '~/components/jet/item/SmallLockupItem.svelte';
+ import GradientOverlay from '~/components/GradientOverlay.svelte';
+ import { colorAsString } from '~/utils/color';
+ import AppEventDate from '~/components/AppEventDate.svelte';
+ import { platform } from '@amp/web-apps-utils';
+ import LaunchNativeButton from '~/components/LaunchNativeButton.svelte';
+
+ export let shelf: AppEventDetailShelf;
+
+ $: item = shelf.items[0];
+ $: ({ appEvent, artwork: productArtwork, video } = item);
+ $: ({ requirements, lockup } = appEvent);
+ $: artwork = video ? video.preview : productArtwork;
+
+ $: backgroundImageUrl = artwork
+ ? buildSrc(
+ artwork.template,
+ {
+ crop: artwork.crop as CropCode,
+ width: 200,
+ height: Math.floor(200 / (artwork.width / artwork.height)),
+ fileType: 'webp',
+ },
+ {},
+ )
+ : undefined;
+
+ $: backgroundColor = artwork?.backgroundColor
+ ? colorAsString(artwork.backgroundColor)
+ : '#000';
+ $: hasLightArtwork = appEvent.mediaOverlayStyle === 'light';
+ $: isXSmallViewport = $mediaQueries === 'xsmall';
+ $: clickAction = lockup?.clickAction;
+ $: urlToLaunchNatively =
+ clickAction && isFlowAction(clickAction) ? clickAction.pageUrl : null;
+ $: shouldShowLaunchNativeButton =
+ platform.ismacOS() &&
+ lockup?.isIOSBinaryMacOSCompatible &&
+ !!urlToLaunchNatively;
+
+ function makeCSSURL(url: string | null | undefined): string {
+ return url ? `url(${url})` : '';
+ }
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false} centered={false}>
+ <div
+ class="event-detail"
+ style:--background-image-url={makeCSSURL(backgroundImageUrl)}
+ style:--background-color={backgroundColor}
+ >
+ {#if video}
+ <div class="video-container">
+ <Video
+ {video}
+ autoplay
+ loop
+ useControls={false}
+ profile="app-event-detail"
+ />
+ </div>
+ {:else if artwork}
+ <div class="artwork-container">
+ <Artwork {artwork} profile="app-event-detail" />
+ </div>
+ {/if}
+
+ {#if isXSmallViewport}
+ <div class="gradient-container">
+ <GradientOverlay
+ --color={backgroundColor}
+ --height="70%"
+ shouldDarken={!hasLightArtwork}
+ />
+ </div>
+ {:else}
+ <div class="tint-container" />
+ {/if}
+
+ <div class="time-container">
+ <AppEventDate {appEvent} />
+ </div>
+
+ <div
+ class="text-container"
+ class:dark={hasLightArtwork && isXSmallViewport}
+ >
+ <div class="event-details-container">
+ <p class="app-event-kind">{appEvent.kind}</p>
+ <h1 class="app-event-title">{appEvent.title}</h1>
+ <p class="app-event-subtitle">
+ {appEvent.detail}
+ </p>
+ {#if requirements}
+ <span class="requirements">{requirements}</span>
+ {/if}
+ </div>
+
+ {#if lockup}
+ <div class="lockup-container">
+ <SmallLockupItem
+ {shouldShowLaunchNativeButton}
+ item={lockup}
+ buttonVariant="transparent"
+ appIconProfile="app-icon"
+ >
+ <svelte:fragment slot="launch-native-button">
+ {#if urlToLaunchNatively}
+ <LaunchNativeButton url={urlToLaunchNatively} />
+ {/if}
+ </svelte:fragment>
+ </SmallLockupItem>
+ </div>
+ {/if}
+ </div>
+ </div>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
+ @use 'ac-sasskit/core/locale' as *;
+
+ .event-detail {
+ --event-image-desktop-width: 31.64%;
+ --border-radius: 16px;
+ --event-gutter: 16px;
+ border-radius: var(--border-radius);
+ display: grid;
+ grid-template-areas:
+ 'time'
+ 'text';
+ grid-template-rows: 1fr auto;
+ aspect-ratio: 9/16;
+ max-height: 90vh;
+ overflow: hidden;
+ position: relative;
+
+ @media (--range-small-up) {
+ --event-gutter: 20px;
+ aspect-ratio: 16/9;
+ background-image: var(--background-image-url);
+ background-position-x: 50%;
+ background-position-y: 50%;
+ background-size: cover;
+ grid-template-areas:
+ 'image time'
+ 'image text';
+ grid-template-columns: var(--event-image-desktop-width) auto;
+ grid-template-rows: auto 1fr;
+ }
+ }
+
+ .artwork-container,
+ .video-container {
+ z-index: 1;
+
+ /* On "mobile" the artwork should be behind both the time and text */
+ grid-row-start: time;
+ grid-row-end: text;
+ grid-column: 1;
+
+ @media (--range-small-up) {
+ /* On large screens, it should be to the right of the text */
+ grid-area: image;
+ }
+ }
+
+ .video-container {
+ background: var(--background-color);
+ color: transparent;
+ }
+
+ .video-container :global(video) {
+ width: unset;
+ position: absolute;
+ }
+
+ .tint-container {
+ background: var(--systemTertiary-onLight_IC);
+ backdrop-filter: saturate(120%) blur(24px);
+ z-index: 1;
+
+ /* One smaller screens, extend behind just the text */
+ grid-area: text;
+
+ /* On larger screens, extend behind time and text */
+ grid-row-start: time;
+ grid-row-end: text;
+ }
+
+ .time-container {
+ grid-area: time;
+ margin-top: var(--event-gutter);
+ margin-inline-start: var(--event-gutter);
+ }
+
+ .time-container :global(time) {
+ color: var(--systemPrimary-onLight);
+ font: var(--callout-emphasized);
+ padding: 3px 10px;
+ background-color: var(--systemSecondary-onDark);
+ border-radius: var(--global-border-radius-medium);
+ position: relative;
+ z-index: 3;
+
+ @media (--range-small-up) {
+ position: relative;
+ z-index: 3;
+ mix-blend-mode: plus-lighter;
+ }
+ }
+
+ .text-container {
+ --blend-mode: plus-lighter;
+ --text-color: var(--systemPrimary-onDark);
+ padding: var(--event-gutter);
+
+ /* Placement within parent */
+ grid-area: text;
+
+ /* Layout of child elements */
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ justify-content: space-between;
+ color: var(--text-color);
+ }
+
+ .text-container.dark {
+ --blend-mode: normal;
+ --text-color: var(--systemPrimary-onLight);
+ }
+
+ .event-details-container {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .app-event-kind {
+ font: var(--callout-emphasized);
+ mix-blend-mode: var(--blend-mode);
+ z-index: 1;
+ }
+
+ .app-event-title {
+ font: var(--large-title-emphasized);
+ text-wrap: pretty;
+ z-index: 1;
+ }
+
+ .app-event-subtitle {
+ font: var(--title-3);
+ z-index: 1;
+ }
+
+ .requirements {
+ position: relative;
+ z-index: 1;
+ font: var(--body-emphasized);
+ }
+
+ .lockup-container {
+ --title-color: var(--text-color);
+ --subtitle-color: var(--text-color);
+ --eyebrow-color: var(--text-color);
+ --linkColor: var(--text-color);
+ --button-blend-mode: var(--blend-mode);
+ border-top: 1px solid var(--systemQuaternary-onDark);
+ padding-top: 16px;
+ z-index: 1;
+ }
+</style>
diff --git a/src/components/jet/shelf/AppPromotionShelf.svelte b/src/components/jet/shelf/AppPromotionShelf.svelte
new file mode 100644
index 0000000..48590cb
--- /dev/null
+++ b/src/components/jet/shelf/AppPromotionShelf.svelte
@@ -0,0 +1,47 @@
+<script lang="ts" context="module">
+ import type {
+ AppPromotion,
+ AppEvent,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ interface AppPromotionShelf extends Shelf {
+ items: AppPromotion[];
+ }
+
+ export function isAppPromotionShelf(
+ shelf: Shelf,
+ ): shelf is AppPromotionShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'appPromotion' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import AppEventItem from '~/components/jet/item/AppEventItem.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import mediaQueries from '~/utils/media-queries';
+
+ export let shelf: AppPromotionShelf;
+
+ $: appEventItems = shelf.items.filter(
+ (item): item is AppEvent => item.promotionType === 'appEvent',
+ );
+ $: isArticleContext = shelf.presentationHints?.isArticleContext;
+ $: gridType =
+ isArticleContext && $mediaQueries !== 'small' ? 'Spotlight' : 'B';
+</script>
+
+<ShelfWrapper {shelf} withTopMargin={isArticleContext}>
+ <ShelfItemLayout
+ shelf={{
+ ...shelf,
+ items: appEventItems,
+ }}
+ {gridType}
+ let:item
+ >
+ <AppEventItem {item} {isArticleContext} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/AppShowcaseShelf.svelte b/src/components/jet/shelf/AppShowcaseShelf.svelte
new file mode 100644
index 0000000..095acf2
--- /dev/null
+++ b/src/components/jet/shelf/AppShowcaseShelf.svelte
@@ -0,0 +1,29 @@
+<script lang="ts" context="module">
+ import type { AppShowcase, Shelf } from '@jet-app/app-store/api/models';
+
+ interface AppShowcaseShelf extends Shelf {
+ contentType: 'appShowcase';
+ items: [AppShowcase];
+ }
+
+ export function isAppShowcaseShelf(
+ shelf: Shelf,
+ ): shelf is AppShowcaseShelf {
+ return (
+ shelf.contentType === 'appShowcase' && Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import SmallLockup from '~/components/jet/item/SmallLockupItem.svelte';
+
+ export let shelf: AppShowcaseShelf;
+
+ $: item = shelf.items[0];
+</script>
+
+<ShelfWrapper {shelf} withTopMargin centered>
+ <SmallLockup item={item.lockup} />
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/AppTrailerLockupShelf.svelte b/src/components/jet/shelf/AppTrailerLockupShelf.svelte
new file mode 100644
index 0000000..f516074
--- /dev/null
+++ b/src/components/jet/shelf/AppTrailerLockupShelf.svelte
@@ -0,0 +1,48 @@
+<script lang="ts" context="module">
+ import type {
+ TrailersLockup,
+ MixedMediaLockup,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ type AppTrailerLockupItem = TrailersLockup | MixedMediaLockup;
+
+ interface AppTrailerLockupShelf extends Shelf {
+ contentType: 'appTrailerLockup';
+ items: AppTrailerLockupItem[];
+ }
+
+ export function isAppTrailerLockupShelf(
+ shelf: Shelf,
+ ): shelf is AppTrailerLockupShelf {
+ return (
+ shelf.contentType === 'appTrailerLockup' &&
+ Array.isArray(shelf.items)
+ );
+ }
+
+ function isMixedMediaLockup(
+ item: AppTrailerLockupItem,
+ ): item is MixedMediaLockup {
+ return Array.isArray(item.trailers);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import MixedMediaLockupItem from '~/components/jet/item/MixedMediaLockupItem.svelte';
+ import TrailersLockupItem from '~/components/jet/item/TrailersLockupItem.svelte';
+
+ export let shelf: AppTrailerLockupShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="B" let:item>
+ {#if isMixedMediaLockup(item)}
+ <MixedMediaLockupItem {item} />
+ {:else}
+ <TrailersLockupItem {item} />
+ {/if}
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/ArcadeFooterShelf.svelte b/src/components/jet/shelf/ArcadeFooterShelf.svelte
new file mode 100644
index 0000000..dc46740
--- /dev/null
+++ b/src/components/jet/shelf/ArcadeFooterShelf.svelte
@@ -0,0 +1,32 @@
+<script lang="ts" context="module">
+ import type { Shelf, ArcadeFooter } from '@jet-app/app-store/api/models';
+
+ interface ArcadeFooterShelf extends Shelf {
+ items: [ArcadeFooter];
+ }
+
+ export function isArcadeFooterShelf(
+ shelf: Shelf,
+ ): shelf is ArcadeFooterShelf {
+ const { contentType, items } = shelf;
+
+ return contentType === 'arcadeFooter' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ArcadeFooterItem from '~/components/jet/item/ArcadeFooterItem.svelte';
+ import HorizontalShelf from '~/components/jet/shelf/HorizontalShelf.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: ArcadeFooterShelf;
+
+ $: gridRows = shelf.rowsPerColumn ?? undefined;
+ $: items = shelf.items;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <HorizontalShelf {gridRows} gridType="Spotlight" {items} let:item>
+ <ArcadeFooterItem {item} />
+ </HorizontalShelf>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/BannerShelf.svelte b/src/components/jet/shelf/BannerShelf.svelte
new file mode 100644
index 0000000..84289c9
--- /dev/null
+++ b/src/components/jet/shelf/BannerShelf.svelte
@@ -0,0 +1,35 @@
+<script lang="ts" context="module">
+ import type { Shelf, Banner } from '@jet-app/app-store/api/models';
+
+ interface BannerShelf extends Shelf {
+ contentType: 'banner';
+ items: Banner[];
+ }
+
+ export function isBannerShelf(shelf: Shelf): shelf is BannerShelf {
+ return shelf.contentType === 'banner' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import BannerItem from '~/components/jet/item/BannerItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: BannerShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <div class="banner-items-container">
+ {#each shelf.items as item}
+ <BannerItem {item} />
+ {/each}
+ </div>
+</ShelfWrapper>
+
+<style>
+ .banner-items-container {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+</style>
diff --git a/src/components/jet/shelf/BrickShelf.svelte b/src/components/jet/shelf/BrickShelf.svelte
new file mode 100644
index 0000000..4bd55e5
--- /dev/null
+++ b/src/components/jet/shelf/BrickShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type { Brick, Shelf } from '@jet-app/app-store/api/models';
+
+ interface BrickShelf extends Shelf {
+ items: Brick[];
+ }
+
+ export function isBrickShelf(shelf: Shelf): shelf is BrickShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'brick' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import BrickItem from '~/components/jet/item/BrickItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: BrickShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout
+ {shelf}
+ gridTypeForShelf="Brick"
+ gridTypeForGrid="F"
+ let:item
+ >
+ <BrickItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/CategoryBrickShelf.svelte b/src/components/jet/shelf/CategoryBrickShelf.svelte
new file mode 100644
index 0000000..22ca86b
--- /dev/null
+++ b/src/components/jet/shelf/CategoryBrickShelf.svelte
@@ -0,0 +1,28 @@
+<script lang="ts" context="module">
+ import type { Brick, Shelf } from '@jet-app/app-store/api/models';
+
+ interface CategoryBrickShelf extends Shelf {
+ items: Brick[];
+ }
+
+ export function isCategoryBrickShelf(
+ shelf: Shelf,
+ ): shelf is CategoryBrickShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'categoryBrick' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import BrickItem from '~/components/jet/item/BrickItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: CategoryBrickShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="C" let:item>
+ <BrickItem {item} shouldOverlayDescription />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/EditorialCardShelf.svelte b/src/components/jet/shelf/EditorialCardShelf.svelte
new file mode 100644
index 0000000..efbd71d
--- /dev/null
+++ b/src/components/jet/shelf/EditorialCardShelf.svelte
@@ -0,0 +1,32 @@
+<script lang="ts" context="module">
+ import type { Shelf, EditorialCard } from '@jet-app/app-store/api/models';
+
+ interface EditorialCardShelf extends Shelf {
+ items: EditorialCard[];
+ }
+
+ export function isEditorialCardShelf(
+ shelf: Shelf,
+ ): shelf is EditorialCardShelf {
+ const { contentType, items } = shelf;
+
+ return contentType === 'editorialCard' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import HeroCarousel from '~/components/hero/Carousel.svelte';
+ import EditorialCardItem from '~/components/jet/item/EditorialCardItem.svelte';
+
+ export let shelf: EditorialCardShelf;
+
+ $: items = shelf.items;
+
+ function deriveBackgroundArtworkFromItem(item: EditorialCard) {
+ return item.artwork;
+ }
+</script>
+
+<HeroCarousel {shelf} {items} {deriveBackgroundArtworkFromItem} let:item>
+ <EditorialCardItem {item} />
+</HeroCarousel>
diff --git a/src/components/jet/shelf/EditorialLinkShelf.svelte b/src/components/jet/shelf/EditorialLinkShelf.svelte
new file mode 100644
index 0000000..0946462
--- /dev/null
+++ b/src/components/jet/shelf/EditorialLinkShelf.svelte
@@ -0,0 +1,122 @@
+<script lang="ts" context="module">
+ import type { Shelf, EditorialLink } from '@jet-app/app-store/api/models';
+
+ interface EditorialLinkShelf extends Shelf {
+ contentType: 'smallStoryCard';
+ items: [EditorialLink];
+ }
+
+ export function isEditorialLinkShelf(
+ shelf: Shelf,
+ ): shelf is EditorialLinkShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'editorialLink' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ChevronRightIcon from '~/sf-symbols/chevron.right.svg';
+ import LinkWrapper from '~/components/LinkWrapper.svelte';
+
+ export let shelf: EditorialLinkShelf;
+ $: item = shelf.items[0];
+ $: ({ clickAction, descriptionText, summaryText } = item);
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <article>
+ <LinkWrapper
+ action={clickAction}
+ includeExternalLinkArrowIcon={false}
+ label={descriptionText}
+ >
+ <svelte:fragment>
+ <div>
+ <span class="title">{descriptionText}</span>
+ <span class="subtitle">{summaryText}</span>
+ </div>
+
+ <span class="icon-container" aria-hidden="true">
+ <ChevronRightIcon />
+ </span>
+ </svelte:fragment>
+ </LinkWrapper>
+ </article>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
+ @use 'ac-sasskit/core/helpers' as *;
+ @use 'ac-sasskit/core/locale' as *;
+
+ article {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 16px;
+ margin: 0 var(--bodyGutter);
+ border-radius: var(--global-border-radius-medium);
+ background-color: var(--systemQuinary);
+ transition: background-color 210ms ease-out;
+ }
+
+ article:hover {
+ cursor: pointer;
+ // a fallback for browsers that don't support relative colors (e.g. the `from` syntax)
+ background-color: var(--systemQuinary);
+ // stylelint-disable-next-line color-function-notation
+ background-color: rgb(
+ from var(--systemQuinary) r g b / calc(alpha + 0.02)
+ );
+ }
+
+ article:hover .icon-container {
+ transform: translateX(2px);
+
+ @include rtl {
+ transform: translateX(-2px) rotate(-180deg);
+ }
+ }
+
+ div {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .title {
+ font: var(--body-emphasized);
+ }
+
+ .subtitle {
+ color: var(--systemSecondary);
+ }
+
+ .icon-container {
+ position: relative;
+ height: 10px;
+ aspect-ratio: 0.9;
+ transition: transform 210ms ease-out;
+
+ @include rtl {
+ transform: rotate(-180deg);
+ }
+ }
+
+ .icon-container :global(path:not([fill='none'])) {
+ fill: var(--systemPrimary);
+ }
+
+ article :global(a) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+</style>
diff --git a/src/components/jet/shelf/FallbackShelf.svelte b/src/components/jet/shelf/FallbackShelf.svelte
new file mode 100644
index 0000000..c7e4200
--- /dev/null
+++ b/src/components/jet/shelf/FallbackShelf.svelte
@@ -0,0 +1,39 @@
+<script lang="ts" context="module">
+ import type { Shelf, ShelfModel } from '@jet-app/app-store/api/models';
+
+ interface FallbackShelf extends Shelf {
+ items: ShelfModel[];
+ }
+
+ export function isFallbackShelf(shelf: Shelf): shelf is FallbackShelf {
+ return Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: FallbackShelf;
+
+ const isPlaceholder = shelf.contentType === 'placeholder';
+</script>
+
+<ShelfWrapper withTopBorder>
+ <ShelfItemLayout {shelf} gridType="C">
+ <div class="wip">
+ {isPlaceholder
+ ? `🔄 Placeholder for ${shelf.placeholderContentType}`
+ : `🚧 ${shelf.contentType}`}
+ </div>
+ </ShelfItemLayout>
+</ShelfWrapper>
+
+<style>
+ .wip {
+ background: #f8f8f8;
+ padding: 16px;
+ border-radius: 8px;
+ border: 1px solid #ccc;
+ }
+</style>
diff --git a/src/components/jet/shelf/FramedArtworkShelf.svelte b/src/components/jet/shelf/FramedArtworkShelf.svelte
new file mode 100644
index 0000000..16f7c48
--- /dev/null
+++ b/src/components/jet/shelf/FramedArtworkShelf.svelte
@@ -0,0 +1,98 @@
+<script lang="ts" context="module">
+ import type { FramedArtwork, Shelf } from '@jet-app/app-store/api/models';
+
+ interface FramedArtworkShelf extends Shelf {
+ contentType: 'framedArtwork';
+ items: [FramedArtwork];
+ }
+
+ export function isFramedArtworkShelf(
+ shelf: Shelf,
+ ): shelf is FramedArtworkShelf {
+ return (
+ shelf.contentType === 'framedArtwork' && Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html';
+ import Artwork, { getNaturalProfile } from '~/components/Artwork.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: FramedArtworkShelf;
+
+ $: item = shelf.items[0];
+ $: ({ artwork, caption, hasRoundedCorners } = item);
+ $: profile = getNaturalProfile(artwork, [1275, 1185, 825, 500, 690]);
+ $: aspectRatio = artwork.width / artwork.height;
+ $: isPortrait = aspectRatio < 1;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <figure
+ class="framed-artwork-item"
+ class:has-rounded-corners={hasRoundedCorners}
+ class:is-portrait={isPortrait}
+ >
+ <div
+ class="artwork-container"
+ style:--aspect-ratio={artwork.width / artwork.height}
+ >
+ <Artwork {artwork} {profile} forceFullWidth={!isPortrait} />
+ </div>
+
+ {#if caption}
+ <figcaption class="caption">
+ {@html sanitizeHtml(caption)}
+ </figcaption>
+ {/if}
+ </figure>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/app/core/globalvars' as *;
+
+ .framed-artwork-item {
+ border-radius: var(--framed-artwork-border-radius);
+ padding: 0 20px;
+ overflow: hidden;
+
+ @media (--sidebar-visible) {
+ padding: 0 20px;
+ }
+
+ @media (--range-small-only) {
+ padding: 0 var(--bodyGutter);
+ }
+ }
+
+ .framed-artwork-item.has-rounded-corners {
+ --framed-artwork-border-radius: var(--global-border-radius-medium);
+ }
+
+ .artwork-container {
+ border-radius: inherit;
+ }
+
+ .caption {
+ border-bottom-left-radius: var(--framed-artwork-border-radius);
+ border-bottom-right-radius: var(--framed-artwork-border-radius);
+ color: var(--systemSecondary);
+ padding: 8px var(--article-page-padding) 0;
+ }
+
+ .framed-artwork-item.is-portrait {
+ --artwork-override-max-height: 560px;
+ --artwork-override-max-width: 100%;
+ --artwork-override-width: auto;
+ }
+
+ .framed-artwork-item.framed-artwork-item.is-portrait .artwork-container {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ max-height: var(--artwork-override-max-height);
+ aspect-ratio: var(--aspect-ratio);
+ }
+</style>
diff --git a/src/components/jet/shelf/FramedVideoShelf.svelte b/src/components/jet/shelf/FramedVideoShelf.svelte
new file mode 100644
index 0000000..a685d39
--- /dev/null
+++ b/src/components/jet/shelf/FramedVideoShelf.svelte
@@ -0,0 +1,78 @@
+<script lang="ts" context="module">
+ import type { FramedVideo, Shelf } from '@jet-app/app-store/api/models';
+
+ interface FramedVideoShelf extends Shelf {
+ contentType: 'framedArtwork';
+ items: [FramedVideo];
+ }
+
+ export function isFramedVideoShelf(
+ shelf: Shelf,
+ ): shelf is FramedVideoShelf {
+ return (
+ shelf.contentType === 'framedVideo' && Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import { getNaturalProfile } from '~/components/Artwork.svelte';
+ import Video from '~/components/jet/Video.svelte';
+
+ export let shelf: FramedVideoShelf;
+
+ $: ({ caption, video } = shelf.items[0]);
+ $: aspectRatio = video.preview.width / video.preview.height;
+ $: profile = getNaturalProfile(video.preview, [608, 528, 608, 928, 298]);
+ $: isPortrait = aspectRatio < 1;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <figure class="framed-artwork-item" class:is-portrait={isPortrait}>
+ <div class="artwork-container" style:--aspect-ratio={aspectRatio}>
+ <Video {video} {profile} autoplay />
+ </div>
+
+ {#if caption}
+ <figcaption class="caption">
+ {@html sanitizeHtml(caption)}
+ </figcaption>
+ {/if}
+ </figure>
+</ShelfWrapper>
+
+<style>
+ .framed-artwork-item {
+ border-radius: var(--global-border-radius-medium);
+ padding: 0 20px;
+ overflow: hidden;
+
+ @media (--sidebar-visible) {
+ padding: 0 20px;
+ }
+
+ @media (--range-small-only) {
+ padding: 0 var(--bodyGutter);
+ }
+ }
+
+ .artwork-container {
+ aspect-ratio: var(--aspect-ratio);
+ overflow: hidden;
+ line-height: 0;
+ border-radius: var(--global-border-radius-medium);
+ background-color: var(--systemQuaternary);
+ max-height: 560px;
+ max-width: 100%;
+ margin: 0 auto;
+ }
+
+ .caption {
+ border-bottom-left-radius: var(--global-border-radius-medium);
+ border-bottom-right-radius: var(--global-border-radius-medium);
+ color: var(--systemSecondary);
+ padding: 8px var(--article-page-padding) 0;
+ }
+</style>
diff --git a/src/components/jet/shelf/HeroCarouselShelf.svelte b/src/components/jet/shelf/HeroCarouselShelf.svelte
new file mode 100644
index 0000000..31a0287
--- /dev/null
+++ b/src/components/jet/shelf/HeroCarouselShelf.svelte
@@ -0,0 +1,38 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ HeroCarousel as HeroCarouselModel,
+ HeroCarouselItem as HeroCarouselItemModel,
+ } from '@jet-app/app-store/api/models';
+
+ interface HeroCarouselShelf extends Shelf {
+ items: [HeroCarouselModel];
+ }
+
+ export function isHeroCarouselShelf(
+ shelf: Shelf,
+ ): shelf is HeroCarouselShelf {
+ const { contentType, items } = shelf;
+
+ return contentType === 'heroCarousel' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import HeroCarousel from '~/components/hero/Carousel.svelte';
+ import HeroCarouselItem from '~/components/jet/item/HeroCarouselItem.svelte';
+ import { isRtl } from '~/utils/locale';
+
+ export let shelf: HeroCarouselShelf;
+
+ $: ({ items: ltrItems, rtlItems } = shelf.items[0]);
+ $: items = isRtl() && rtlItems.length ? rtlItems : ltrItems;
+
+ function deriveBackgroundArtworkFromItem(item: HeroCarouselItemModel) {
+ return item.artwork || item.video?.preview;
+ }
+</script>
+
+<HeroCarousel {shelf} {items} {deriveBackgroundArtworkFromItem} let:item>
+ <HeroCarouselItem {item} />
+</HeroCarousel>
diff --git a/src/components/jet/shelf/HorizontalRuleShelf.svelte b/src/components/jet/shelf/HorizontalRuleShelf.svelte
new file mode 100644
index 0000000..3313ff2
--- /dev/null
+++ b/src/components/jet/shelf/HorizontalRuleShelf.svelte
@@ -0,0 +1,54 @@
+<script lang="ts" context="module">
+ import type {
+ HorizontalRule,
+ HorizontalRuleStyle,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ export interface HorizontalRuleShelf extends Shelf {
+ contentType: 'horizontalRule';
+ items: [HorizontalRule];
+ }
+
+ export function isHorizontalRuleShelf(
+ shelf: Shelf,
+ ): shelf is HorizontalRuleShelf {
+ return (
+ shelf.contentType === 'horizontalRule' && Array.isArray(shelf.items)
+ );
+ }
+
+ function horizontalRuleStyleToBorderStyle(
+ style: HorizontalRuleStyle,
+ ): string {
+ return style.toLowerCase();
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import { colorAsString } from '~/utils/color';
+
+ export let shelf: HorizontalRuleShelf;
+
+ $: item = shelf.items[0];
+ $: borderStyle = horizontalRuleStyleToBorderStyle(item.style);
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <hr
+ style:color={colorAsString(item.color)}
+ style:border-style={borderStyle}
+ />
+</ShelfWrapper>
+
+<style>
+ hr {
+ display: block;
+ height: 1px;
+ border-width: 1px 0 0;
+ border-color: currentColor;
+ margin: 1em var(--bodyGutter);
+ padding: 0;
+ }
+</style>
diff --git a/src/components/jet/shelf/HorizontalShelf.svelte b/src/components/jet/shelf/HorizontalShelf.svelte
new file mode 100644
index 0000000..1addb31
--- /dev/null
+++ b/src/components/jet/shelf/HorizontalShelf.svelte
@@ -0,0 +1,53 @@
+<script lang="ts">
+ import type { Opt } from '@jet/environment';
+ import Shelf from '@amp/web-app-components/src/components/Shelf/Shelf.svelte';
+ import type {
+ ArrowOffset,
+ GridType,
+ } from '@amp/web-app-components/src/components/Shelf/types';
+ import { getI18n } from '~/stores/i18n';
+
+ type T = $$Generic;
+
+ export let items: T[];
+ export let gridType: GridType;
+ export let gridRows: number = 1;
+ export let arrowOffset: Opt<ArrowOffset> = null;
+
+ const i18n = getI18n();
+ // This makes the let:item of type T, because it doesn't know type when it comes back from the Shelf component.
+ function castGenericItem(x: T): T {
+ return x;
+ }
+</script>
+
+<div class="horizontal-shelf" data-test-id="horizontal-shelf">
+ <Shelf translateFn={$i18n.t} {items} {gridType} {gridRows} {arrowOffset}>
+ <svelte:fragment slot="item" let:item let:index let:numberOfItems>
+ <slot item={castGenericItem(item)} {index} {numberOfItems} />
+ </svelte:fragment>
+ </Shelf>
+</div>
+
+<style>
+ .horizontal-shelf :global(.shelf-grid) {
+ --shelfGridPaddingInline: var(--bodyGutter);
+ --shelfGridGutterWidth: var(--bodyGutter);
+ }
+
+ .horizontal-shelf :global(.shelf-grid__list) {
+ @media (--range-xsmall-only) {
+ scroll-padding-inline-start: var(
+ --shelfScrollPaddingInline,
+ var(--bodyGutter)
+ );
+ }
+ }
+
+ .horizontal-shelf
+ :global(.shelf-grid__list--grid-type-Spotlight .shelf-grid__list-item) {
+ @media (--range-xsmall-only) {
+ --standard-lockup-shadow-offset: var(--shelfScrollPaddingInline, 0);
+ }
+ }
+</style>
diff --git a/src/components/jet/shelf/InAppPurchaseLockupShelf.svelte b/src/components/jet/shelf/InAppPurchaseLockupShelf.svelte
new file mode 100644
index 0000000..bf2e75e
--- /dev/null
+++ b/src/components/jet/shelf/InAppPurchaseLockupShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type {
+ InAppPurchaseLockup,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ interface InAppPurchaseLockupShelf extends Shelf {
+ items: InAppPurchaseLockup[];
+ }
+
+ export function isInAppPurchaseLockupShelf(
+ shelf: Shelf,
+ ): shelf is InAppPurchaseLockupShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'inAppPurchaseLockup' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import InAppPurchaseLockupComponent from '~/components/jet/item/InAppPurchaseLockup.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: InAppPurchaseLockupShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="InAppPurchaseLockup" let:item>
+ <InAppPurchaseLockupComponent {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/LargeBrickShelf.svelte b/src/components/jet/shelf/LargeBrickShelf.svelte
new file mode 100644
index 0000000..eea1044
--- /dev/null
+++ b/src/components/jet/shelf/LargeBrickShelf.svelte
@@ -0,0 +1,26 @@
+<script lang="ts" context="module">
+ import type { Brick, Shelf } from '@jet-app/app-store/api/models';
+
+ interface LargeBrickShelf extends Shelf {
+ items: Brick[];
+ }
+
+ export function isLargeBrickShelf(shelf: Shelf): shelf is LargeBrickShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'largeBrick' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import LargeBrickItem from '~/components/jet/item/LargeBrickItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: LargeBrickShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="LargeBrick" let:item>
+ <LargeBrickItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/LargeHeroBreakoutShelf.svelte b/src/components/jet/shelf/LargeHeroBreakoutShelf.svelte
new file mode 100644
index 0000000..a0dfe9c
--- /dev/null
+++ b/src/components/jet/shelf/LargeHeroBreakoutShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type {
+ LargeHeroBreakout,
+ Shelf,
+ } from '@jet-app/app-store/api/models';
+
+ interface LargeHeroBreakoutShelf extends Shelf {
+ items: LargeHeroBreakout[];
+ }
+
+ export function isLargeHeroBreakoutShelf(
+ shelf: Shelf,
+ ): shelf is LargeHeroBreakoutShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'largeHeroBreakout' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import LargeHeroBreakoutItem from '~/components/jet/item/LargeHeroBreakoutItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: LargeHeroBreakoutShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="Spotlight" let:item>
+ <LargeHeroBreakoutItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/LargeImageLockupShelf.svelte b/src/components/jet/shelf/LargeImageLockupShelf.svelte
new file mode 100644
index 0000000..fd192fb
--- /dev/null
+++ b/src/components/jet/shelf/LargeImageLockupShelf.svelte
@@ -0,0 +1,30 @@
+<script lang="ts" context="module">
+ import type { Shelf, ImageLockup } from '@jet-app/app-store/api/models';
+
+ interface LargeImageLockupShelf extends Shelf {
+ items: ImageLockup[];
+ }
+
+ export function isLargeImageLockupShelf(
+ shelf: Shelf,
+ ): shelf is LargeImageLockupShelf {
+ return (
+ shelf.contentType === 'largeImageLockup' &&
+ Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import LargeImageLockupItem from '~/components/jet/item/LargeImageLockupItem.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: LargeImageLockupShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="A" let:item>
+ <LargeImageLockupItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/LargeLockupShelf.svelte b/src/components/jet/shelf/LargeLockupShelf.svelte
new file mode 100644
index 0000000..dedd1fe
--- /dev/null
+++ b/src/components/jet/shelf/LargeLockupShelf.svelte
@@ -0,0 +1,28 @@
+<script lang="ts" context="module">
+ import type { Lockup, Shelf } from '@jet-app/app-store/api/models';
+
+ interface LargeLockupShelf extends Shelf {
+ items: Lockup[];
+ }
+
+ export function isLargeLockupShelf(
+ shelf: Shelf,
+ ): shelf is LargeLockupShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'largeLockup' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import LargeLockupItem from '~/components/jet/item/LargeLockupItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: LargeLockupShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="LargeLockup" let:item>
+ <LargeLockupItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/LargeStoryCardShelf.svelte b/src/components/jet/shelf/LargeStoryCardShelf.svelte
new file mode 100644
index 0000000..c1a1e57
--- /dev/null
+++ b/src/components/jet/shelf/LargeStoryCardShelf.svelte
@@ -0,0 +1,32 @@
+<script lang="ts" context="module">
+ import type { Shelf, TodayCard } from '@jet-app/app-store/api/models';
+
+ interface LargeStoryCardShelf extends Shelf {
+ items: TodayCard[];
+ }
+
+ export function isLargeStoryCardShelf(
+ shelf: Shelf,
+ ): shelf is LargeStoryCardShelf {
+ return (
+ shelf.contentType === 'largeStoryCard' && Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import HeroCarousel from '~/components/hero/Carousel.svelte';
+ import LargeStoryCardItem from '~/components/jet/item/LargeStoryCardItem.svelte';
+
+ export let shelf: LargeStoryCardShelf;
+
+ $: items = shelf.items;
+
+ function deriveBackgroundArtworkFromItem(item: TodayCard) {
+ return item.heroMedia?.artworks[0];
+ }
+</script>
+
+<HeroCarousel {shelf} {items} {deriveBackgroundArtworkFromItem} let:item>
+ <LargeStoryCardItem {item} />
+</HeroCarousel>
diff --git a/src/components/jet/shelf/LinkableTextShelf.svelte b/src/components/jet/shelf/LinkableTextShelf.svelte
new file mode 100644
index 0000000..dcfde36
--- /dev/null
+++ b/src/components/jet/shelf/LinkableTextShelf.svelte
@@ -0,0 +1,43 @@
+<script lang="ts" context="module">
+ import type { Shelf, LinkableText } from '@jet-app/app-store/api/models';
+
+ interface LinkableTextShelf extends Shelf {
+ contentType: 'linkableText';
+ items: [LinkableText];
+ }
+
+ export function isLinkableTextShelf(
+ shelf: Shelf,
+ ): shelf is LinkableTextShelf {
+ return (
+ shelf.contentType === 'linkableText' && Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ import LinkableTextItem from '~/components/jet/item/LinkableTextItem.svelte';
+
+ export let shelf: LinkableTextShelf;
+</script>
+
+<ShelfWrapper centered withPaddingTop={true}>
+ <div class="banner">
+ <LinkableTextItem item={shelf.items[0]} />
+ </div>
+</ShelfWrapper>
+
+<style>
+ .banner {
+ background: rgba(var(--keyColor-rgb), 0.07);
+ padding: 8px 16px;
+ text-align: center;
+ border-radius: var(--global-border-radius-small);
+ }
+
+ .banner :global(a) {
+ color: var(--keyColor);
+ }
+</style>
diff --git a/src/components/jet/shelf/MarkerShelf.svelte b/src/components/jet/shelf/MarkerShelf.svelte
new file mode 100644
index 0000000..c719235
--- /dev/null
+++ b/src/components/jet/shelf/MarkerShelf.svelte
@@ -0,0 +1,36 @@
+<script lang="ts" context="module">
+ import type { ShelfBasedProductPage } from '@jet-app/app-store/api/models';
+ import type {
+ Lockup,
+ Shelf,
+ ShelfMarker,
+ } from '@jet-app/app-store/api/models';
+
+ export interface MarkerShelf extends Shelf {
+ contentType: 'marker';
+ marker: ShelfMarker;
+ items: Lockup[];
+ }
+
+ export function isMarkerShelf(shelf: Shelf): shelf is MarkerShelf {
+ const { contentType, marker, items } = shelf;
+
+ return (
+ contentType === 'marker' &&
+ typeof marker === 'string' &&
+ Array.isArray(items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import ProductTopLockup from '~/components/jet/marker-shelf/ProductTopLockup.svelte';
+
+ export let shelf: MarkerShelf;
+
+ export let page: ShelfBasedProductPage;
+</script>
+
+{#if shelf.marker === 'productTopLockup'}
+ <ProductTopLockup {page} />
+{/if}
diff --git a/src/components/jet/shelf/MediumImageLockupShelf.svelte b/src/components/jet/shelf/MediumImageLockupShelf.svelte
new file mode 100644
index 0000000..f7b1316
--- /dev/null
+++ b/src/components/jet/shelf/MediumImageLockupShelf.svelte
@@ -0,0 +1,28 @@
+<script lang="ts" context="module">
+ import type { ImageLockup, Shelf } from '@jet-app/app-store/api/models';
+
+ interface MediumImageLockupShelf extends Shelf {
+ items: ImageLockup[];
+ }
+
+ export function isMediumImageLockupShelf(
+ shelf: Shelf,
+ ): shelf is MediumImageLockupShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'mediumImageLockup' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import MediumImageLockupItem from '~/components/jet/item/MediumImageLockupItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: MediumImageLockupShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="B" let:item>
+ <MediumImageLockupItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/MediumLockupShelf.svelte b/src/components/jet/shelf/MediumLockupShelf.svelte
new file mode 100644
index 0000000..186acb2
--- /dev/null
+++ b/src/components/jet/shelf/MediumLockupShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type { Lockup, Shelf } from '@jet-app/app-store/api/models';
+
+ interface MediumLockupShelf extends Shelf {
+ items: Lockup[];
+ }
+
+ export function isMediumLockupShelf(
+ shelf: Shelf,
+ ): shelf is MediumLockupShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'mediumLockup' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import MediumLockupItem from '~/components/jet/item/MediumLockupItem.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: MediumLockupShelf;
+
+ $: isArticleContext = shelf.presentationHints?.isArticleContext;
+ $: gridType = isArticleContext ? 'Spotlight' : 'MediumLockup';
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} {gridType} rowsPerColumnOverride={2} let:item>
+ <MediumLockupItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/MediumStoryCardShelf.svelte b/src/components/jet/shelf/MediumStoryCardShelf.svelte
new file mode 100644
index 0000000..35c3ec3
--- /dev/null
+++ b/src/components/jet/shelf/MediumStoryCardShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type { Shelf } from '@jet-app/app-store/api/models';
+
+ import MediumStoryCardItem, {
+ type Item as MediumStoryCardItemModel,
+ } from '~/components/jet/item/MediumStoryCardItem.svelte';
+
+ interface MediumStoryCardShelf extends Shelf {
+ items: MediumStoryCardItemModel[];
+ }
+
+ export function isMediumStoryCardShelf(
+ shelf: Shelf,
+ ): shelf is MediumStoryCardShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'mediumStoryCard' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: MediumStoryCardShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="B" let:item>
+ <MediumStoryCardItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/PageHeaderShelf.svelte b/src/components/jet/shelf/PageHeaderShelf.svelte
new file mode 100644
index 0000000..59c99b2
--- /dev/null
+++ b/src/components/jet/shelf/PageHeaderShelf.svelte
@@ -0,0 +1,34 @@
+<script lang="ts" context="module">
+ import type { PageHeader, Shelf } from '@jet-app/app-store/api/models';
+
+ interface PageHeaderShelf extends Shelf {
+ items: [PageHeader];
+ }
+
+ export function isPageHeaderShelf(shelf: Shelf): shelf is PageHeaderShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'pageHeader' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfTitle from '~/components/Shelf/Title.svelte';
+
+ export let shelf: PageHeaderShelf;
+
+ $: [item] = shelf.items;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <div class="shelf-title-wrapper" slot="title">
+ <ShelfTitle title={item.title} subtitle={item.subtitle} />
+ </div>
+</ShelfWrapper>
+
+<style>
+ .shelf-title-wrapper {
+ --shelf-title-font: var(--title-1-emphasized);
+ display: contents;
+ }
+</style>
diff --git a/src/components/jet/shelf/ParagraphShelf.svelte b/src/components/jet/shelf/ParagraphShelf.svelte
new file mode 100644
index 0000000..777338e
--- /dev/null
+++ b/src/components/jet/shelf/ParagraphShelf.svelte
@@ -0,0 +1,52 @@
+<script lang="ts" context="module">
+ import type { Paragraph, Shelf } from '@jet-app/app-store/api/models';
+
+ interface ParagraphShelf extends Shelf {
+ contentType: 'paragraph';
+ items: Paragraph[];
+ }
+
+ export function isParagraphShelf(shelf: Shelf): shelf is ParagraphShelf {
+ return shelf.contentType === 'paragraph' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ParagraphShelfItem from '~/components/jet/item/ParagraphShelfItem.svelte';
+
+ export let shelf: ParagraphShelf;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <div slot="title" class="title-container">
+ {#if shelf.title}
+ <h2>{shelf.title}</h2>
+ {/if}
+ </div>
+
+ <div class="content-container">
+ {#each shelf.items as item}
+ <ParagraphShelfItem {item} />
+ {/each}
+ </div>
+</ShelfWrapper>
+
+<style>
+ h2 {
+ color: var(--systemPrimary);
+ font: var(--title-2-emphasized);
+ text-wrap: pretty;
+ margin: 16px 0;
+ }
+
+ .title-container,
+ .content-container {
+ margin: 0 var(--bodyGutter);
+ }
+
+ /* Whenever this shelf is nested in a modal, we don't want to add extra margin since the modal provides its own */
+ :global(.modal-content) .content-container {
+ margin: unset;
+ }
+</style>
diff --git a/src/components/jet/shelf/PosterLockupShelf.svelte b/src/components/jet/shelf/PosterLockupShelf.svelte
new file mode 100644
index 0000000..101c1d6
--- /dev/null
+++ b/src/components/jet/shelf/PosterLockupShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type { Shelf, PosterLockup } from '@jet-app/app-store/api/models';
+
+ interface PosterLockupShelf extends Shelf {
+ items: PosterLockup[];
+ }
+
+ export function isPosterLockupShelf(
+ shelf: Shelf,
+ ): shelf is PosterLockupShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'posterLockup' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import mediaQueries from '~/utils/media-queries';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import PosterLockupItem from '~/components/jet/item/PosterLockupItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: PosterLockupShelf;
+
+ $: gridType = $mediaQueries === 'xsmall' ? 'Spotlight' : 'PosterLockup';
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} {gridType} let:item>
+ <PosterLockupItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/PrivacyFooterShelf.svelte b/src/components/jet/shelf/PrivacyFooterShelf.svelte
new file mode 100644
index 0000000..dccade6
--- /dev/null
+++ b/src/components/jet/shelf/PrivacyFooterShelf.svelte
@@ -0,0 +1,40 @@
+<script lang="ts" context="module">
+ import type { PrivacyFooter, Shelf } from '@jet-app/app-store/api/models';
+
+ interface PrivacyFooterShelf extends Shelf {
+ items: [PrivacyFooter];
+ }
+
+ export function isPrivacyFooterShelf(
+ shelf: Shelf,
+ ): shelf is PrivacyFooterShelf {
+ let { contentType, items } = shelf;
+
+ return contentType === 'privacyFooter' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import LinkableTextItem from '~/components/jet/item/LinkableTextItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: PrivacyFooterShelf;
+
+ $: bodyText = shelf.items[0].bodyText;
+</script>
+
+<ShelfWrapper {shelf} centered>
+ <p>
+ <LinkableTextItem item={bodyText} />
+ </p>
+</ShelfWrapper>
+
+<style>
+ p {
+ font: var(--body-tall);
+ }
+
+ p :global(a) {
+ color: var(--keyColor);
+ }
+</style>
diff --git a/src/components/jet/shelf/PrivacyHeaderShelf.svelte b/src/components/jet/shelf/PrivacyHeaderShelf.svelte
new file mode 100644
index 0000000..5ace666
--- /dev/null
+++ b/src/components/jet/shelf/PrivacyHeaderShelf.svelte
@@ -0,0 +1,145 @@
+<script lang="ts" context="module">
+ import {
+ type Action,
+ type FlowAction,
+ type GenericPage,
+ type PrivacyHeader,
+ type Shelf,
+ isFlowAction,
+ } from '@jet-app/app-store/api/models';
+
+ import {
+ isPrivacyTypeShelf,
+ type PrivacyTypeShelf,
+ } from '~/components/jet/shelf/PrivacyTypeShelf.svelte';
+
+ interface PrivacyHeaderShelf extends Shelf {
+ items: [PrivacyHeader];
+ }
+
+ interface PrivacyDetailPage extends GenericPage {
+ shelves: (PrivacyTypeShelf | PrivacyHeaderShelf)[];
+ }
+
+ interface PrivacyDetailPageFlowAction extends FlowAction {
+ page: 'privacyDetail';
+ pageData: PrivacyDetailPage;
+ }
+
+ export function isPrivacyHeaderShelf(
+ shelf: Shelf,
+ ): shelf is PrivacyHeaderShelf {
+ let { contentType, items } = shelf;
+ return contentType === 'privacyHeader' && Array.isArray(items);
+ }
+
+ function isPrivacyDetailFlowAction(
+ action: Action,
+ ): action is PrivacyDetailPageFlowAction {
+ return isFlowAction(action) && action.page === 'privacyDetail';
+ }
+</script>
+
+<script lang="ts">
+ import Modal from '@amp/web-app-components/src/components/Modal/Modal.svelte';
+ import ContentModal from '~/components/jet/item/ContentModal.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfTitle from '~/components/Shelf/Title.svelte';
+ import PrivacyHeaderItem from '~/components/jet/item/PrivacyHeaderItem.svelte';
+ import PrivacyTypeItem from '~/components/jet/item/PrivacyTypeItem.svelte';
+ import { getI18n } from '~/stores/i18n';
+ import { APP_PRIVACY_MODAL_ID } from '~/utils/metrics';
+
+ export let shelf: PrivacyHeaderShelf;
+
+ let modalComponent: Modal | undefined;
+ let modalTriggerElement: HTMLElement | null = null;
+
+ const { seeAllAction } = shelf;
+ const i18n = getI18n();
+ const translateFn = (key: string) => $i18n.t(key);
+ const handleModalClose = () => modalComponent?.close();
+ const handleOpenModalClick = (e: Event) => {
+ modalTriggerElement = e.target as HTMLElement;
+ modalComponent?.showModal();
+ };
+
+ const destination =
+ seeAllAction && isPrivacyDetailFlowAction(seeAllAction)
+ ? seeAllAction
+ : undefined;
+ const pageData = destination?.pageData;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <div slot="title" class="title-container">
+ {#if shelf.title}
+ <button on:click={handleOpenModalClick}>
+ <ShelfTitle title={shelf.title} seeAllAction={destination} />
+ </button>
+ {/if}
+
+ {#if pageData}
+ <Modal {modalTriggerElement} bind:this={modalComponent}>
+ <ContentModal
+ {translateFn}
+ on:close={handleModalClose}
+ title={pageData.title || null}
+ subtitle={null}
+ targetId={APP_PRIVACY_MODAL_ID}
+ >
+ <svelte:fragment slot="content">
+ <ul class="modal-content-container">
+ {#each pageData.shelves as shelf}
+ {#if isPrivacyHeaderShelf(shelf)}
+ {#each shelf.items as item}
+ <PrivacyHeaderItem {item} />
+ {/each}
+ {/if}
+
+ {#if isPrivacyTypeShelf(shelf)}
+ {#each shelf.items as item}
+ <PrivacyTypeItem
+ {item}
+ isDetailView={true}
+ />
+ {/each}
+ {/if}
+ {/each}
+ </ul>
+ </svelte:fragment>
+ </ContentModal>
+ </Modal>
+ {/if}
+ </div>
+
+ <div class="header-container">
+ <div>
+ <PrivacyHeaderItem item={shelf.items[0]} />
+ </div>
+ </div>
+</ShelfWrapper>
+
+<style>
+ .title-container {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 16px;
+ padding-inline-end: var(--bodyGutter);
+ }
+
+ .header-container {
+ margin: 0 var(--bodyGutter);
+ }
+
+ .header-container div {
+ @media (--range-medium-up) {
+ width: 66%;
+ }
+ }
+
+ .modal-content-container {
+ font: var(--body-tall);
+ white-space: normal;
+ }
+</style>
diff --git a/src/components/jet/shelf/PrivacyTypeShelf.svelte b/src/components/jet/shelf/PrivacyTypeShelf.svelte
new file mode 100644
index 0000000..3817251
--- /dev/null
+++ b/src/components/jet/shelf/PrivacyTypeShelf.svelte
@@ -0,0 +1,29 @@
+<script lang="ts" context="module">
+ import type { PrivacyType, Shelf } from '@jet-app/app-store/api/models';
+
+ export interface PrivacyTypeShelf extends Shelf {
+ items: PrivacyType[];
+ }
+
+ export function isPrivacyTypeShelf(
+ shelf: Shelf,
+ ): shelf is PrivacyTypeShelf {
+ let { contentType, items } = shelf;
+
+ return contentType === 'privacyType' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import PrivacyTypeItem from '~/components/jet/item/PrivacyTypeItem.svelte';
+
+ export let shelf: PrivacyTypeShelf;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <ShelfItemLayout {shelf} gridType="B" let:item>
+ <PrivacyTypeItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/ProductBadgeShelf.svelte b/src/components/jet/shelf/ProductBadgeShelf.svelte
new file mode 100644
index 0000000..cded0b7
--- /dev/null
+++ b/src/components/jet/shelf/ProductBadgeShelf.svelte
@@ -0,0 +1,59 @@
+<script lang="ts" context="module">
+ import type { Badge, Shelf } from '@jet-app/app-store/api/models';
+
+ interface ProductBadgeShelf extends Shelf {
+ items: Badge[];
+ }
+
+ export function isProductBadgeShelf(
+ shelf: Shelf,
+ ): shelf is ProductBadgeShelf {
+ const { contentType, items } = shelf || {};
+ return contentType === 'productBadge' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ProductBadgeItem from '~/components/jet/item/ProductBadgeItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: ProductBadgeShelf;
+
+ $: shelf.items = shelf.items.filter(
+ (item) => item.type !== 'friendsPlaying',
+ );
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false} withTopMargin={true}>
+ <div class="inforibbon-shelf-wrapper">
+ <ShelfItemLayout {shelf} gridType="ProductBadge" let:item>
+ <ProductBadgeItem {item} />
+ </ShelfItemLayout>
+ </div>
+</ShelfWrapper>
+
+<style lang="scss">
+ .inforibbon-shelf-wrapper {
+ padding-bottom: 16px;
+ }
+
+ .inforibbon-shelf-wrapper :global(ul) {
+ display: grid;
+
+ /*
+ Here we are overriding the grid template styles from `ShelfItemLayout -> Grid`,
+ to make it so the badge row always takes up the full-width of the browser until
+ when not in the XS/mobile view.
+ */
+ @media (--range-small-up) {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+
+ // prevent collapse of focus outlines
+ .inforibbon-shelf-wrapper :global(a) {
+ display: block;
+ }
+</style>
diff --git a/src/components/jet/shelf/ProductCapabilityShelf.svelte b/src/components/jet/shelf/ProductCapabilityShelf.svelte
new file mode 100644
index 0000000..6a4307a
--- /dev/null
+++ b/src/components/jet/shelf/ProductCapabilityShelf.svelte
@@ -0,0 +1,31 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ ProductCapability,
+ } from '@jet-app/app-store/api/models';
+
+ interface ProductCapabilityShelf extends Shelf {
+ items: ProductCapability[];
+ }
+
+ export function isProductCapabilityShelf(
+ shelf: Shelf,
+ ): shelf is ProductCapabilityShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'productCapability' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ProductCapabilityItem from '../item/ProductCapabilityItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: ProductCapabilityShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="SearchLink" let:item>
+ <ProductCapabilityItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/ProductDescriptionShelf.svelte b/src/components/jet/shelf/ProductDescriptionShelf.svelte
new file mode 100644
index 0000000..7cddcee
--- /dev/null
+++ b/src/components/jet/shelf/ProductDescriptionShelf.svelte
@@ -0,0 +1,95 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ ProductDescription,
+ } from '@jet-app/app-store/api/models';
+
+ interface ProductDescriptionShelf extends Shelf {
+ items: [ProductDescription];
+ }
+
+ export function isProductDescriptionShelf(
+ shelf: Shelf,
+ ): shelf is ProductDescriptionShelf {
+ const { contentType, items } = shelf;
+
+ return contentType === 'productDescription' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html';
+ import LineClamp from '@amp/web-app-components/src/components/LineClamp/LineClamp.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import { getI18n } from '~/stores/i18n';
+
+ export let shelf: ProductDescriptionShelf;
+
+ const i18n = getI18n();
+ const description = shelf.items[0]?.paragraph.text;
+ const handleMoreClick = () => (isOpen = true);
+ let isOpen = false;
+
+ function handleLineClampResize(event: CustomEvent) {
+ if (!event.detail.truncated) {
+ isOpen = true;
+ }
+ }
+</script>
+
+<ShelfWrapper centered>
+ <article>
+ <p>
+ {#if isOpen}
+ {@html sanitizeHtml(description)}
+ {:else}
+ <LineClamp observe clamp={5} on:resize={handleLineClampResize}>
+ {@html sanitizeHtml(description)}
+ </LineClamp>
+ {/if}
+
+ {#if !isOpen}
+ <button on:click={handleMoreClick}>
+ {$i18n.t('ASE.Web.AppStore.More')}
+ </button>
+ {/if}
+ </p>
+ </article>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
+ @use 'ac-sasskit/core/locale' as *;
+
+ p {
+ white-space: break-spaces;
+ font: var(--body-tall);
+ position: relative;
+ display: flex;
+ flex-direction: column;
+
+ @media (--range-medium-up) {
+ width: 66%;
+ }
+ }
+
+ button {
+ --gradient-direction: 270deg;
+ display: flex;
+ justify-content: end;
+ position: absolute;
+ bottom: 0;
+ inset-inline-end: 0;
+ padding-inline-start: 20px;
+ color: var(--keyColor);
+ background: linear-gradient(
+ var(--gradient-direction),
+ var(--pageBg) 72%,
+ transparent 100%
+ );
+
+ @include rtl {
+ --gradient-direction: 90deg;
+ }
+ }
+</style>
diff --git a/src/components/jet/shelf/ProductMediaShelf.svelte b/src/components/jet/shelf/ProductMediaShelf.svelte
new file mode 100644
index 0000000..f57fee7
--- /dev/null
+++ b/src/components/jet/shelf/ProductMediaShelf.svelte
@@ -0,0 +1,269 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ ProductMedia,
+ AppPlatform,
+ MediaType,
+ MediaPlatform,
+ } from '@jet-app/app-store/api/models';
+
+ interface ProductMediaShelf extends Shelf, ProductMedia {
+ items: ProductMedia['items'];
+ expandedMedia?: ProductMediaShelf[];
+ }
+
+ export function isProductMediaShelf(
+ shelf: Shelf,
+ ): shelf is ProductMediaShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'productMediaItem' && Array.isArray(items);
+ }
+
+ const platformToIconNameMap: Record<AppPlatform, string> = {
+ phone: 'iphone.gen2',
+ pad: 'ipad.gen2',
+ tv: 'tv',
+ watch: 'applewatch',
+ mac: 'macbook.gen2',
+ messages: 'message',
+ vision: 'visionpro',
+ };
+
+ const platformToDescriptionMap: Record<AppPlatform, string> = {
+ phone: 'AppStore.AppPlatform.Phone',
+ pad: 'AppStore.AppPlatform.Pad',
+ tv: 'AppStore.AppPlatform.TV',
+ watch: 'AppStore.AppPlatform.Watch',
+ mac: 'AppStore.AppPlatform.Mac',
+ messages: 'AppStore.AppPlatform.Messages',
+ vision: 'AppStore.AppPlatform.Vision',
+ };
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ProductMediaVisionItem from '~/components/jet/item/ProductMedia/ProductMediaVisionItem.svelte';
+ import ProductMediaPhoneItem from '~/components/jet/item/ProductMedia/ProductMediaPhoneItem.svelte';
+ import ProductMediaMacItem from '~/components/jet/item/ProductMedia/ProductMediaMacItem.svelte';
+ import ProductMediaPadItem from '~/components/jet/item/ProductMedia/ProductMediaPadItem.svelte';
+ import ProductMediaWatchItem from '~/components/jet/item/ProductMedia/ProductMediaWatchItem.svelte';
+ import ProductMediaTVItem from '~/components/jet/item/ProductMedia/ProductMediaTVItem.svelte';
+ import ChevronDown from '~/sf-symbols/chevron.down.svg';
+ import SFSymbol from '~/components/SFSymbol.svelte';
+ import { getI18n } from '~/stores/i18n';
+ import { slide } from 'svelte/transition';
+ import { getJet } from '~/jet';
+
+ export let shelf: ProductMediaShelf;
+ export let isExpandedMedia: boolean = false;
+
+ const i18n = getI18n();
+ const jet = getJet();
+
+ let appPlatform: AppPlatform | undefined;
+ let allPlatforms: MediaPlatform[] | undefined;
+ let mediaType: MediaType | undefined;
+ let hasPortraitMedia: boolean = false;
+ let shouldDisplayExpandedMedia: boolean = false;
+
+ $: {
+ if (shelf.contentsMetadata.type === 'productMedia') {
+ ({ hasPortraitMedia, allPlatforms } = shelf.contentsMetadata);
+ ({ appPlatform, mediaType } = shelf.contentsMetadata.platform);
+ }
+ }
+
+ $: allPlatformsDescription = allPlatforms
+ ?.map(({ appPlatform }) =>
+ $i18n.t(platformToDescriptionMap[appPlatform]),
+ )
+ ?.join($i18n.t('AppStore.AppPlatform.Component.Separator'));
+
+ $: shouldShowPlatform =
+ isExpandedMedia ||
+ shouldDisplayExpandedMedia ||
+ allPlatforms?.length === 1;
+
+ const displayExpandedMedia = () => {
+ shouldDisplayExpandedMedia = true;
+ jet.recordCustomMetricsEvent({
+ eventType: 'click',
+ actionDetails: { type: 'platformSelect' },
+ targetType: 'button',
+ targetId: 'productMediaShelf',
+ });
+ };
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={!shelf.expandedMedia}>
+ {#if appPlatform === 'vision'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotVision" let:item>
+ <ProductMediaVisionItem {item} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'phone' || appPlatform === 'messages'}
+ <ShelfItemLayout
+ {shelf}
+ gridType={hasPortraitMedia ? 'ScreenshotPhone' : 'ScreenshotLarge'}
+ let:item
+ >
+ <ProductMediaPhoneItem {item} {hasPortraitMedia} {mediaType} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'pad'}
+ <ShelfItemLayout
+ {shelf}
+ gridType={hasPortraitMedia ? 'ScreenshotPad' : 'ScreenshotLarge'}
+ let:item
+ >
+ <ProductMediaPadItem {item} {hasPortraitMedia} {mediaType} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'mac'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotLarge" let:item>
+ <ProductMediaMacItem {item} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'tv'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotLarge" let:item>
+ <ProductMediaTVItem {item} />
+ </ShelfItemLayout>
+ {:else if appPlatform === 'watch'}
+ <ShelfItemLayout {shelf} gridType="ScreenshotPhone" let:item>
+ <ProductMediaWatchItem {item} {mediaType} />
+ </ShelfItemLayout>
+ {:else}
+ <ShelfItemLayout {shelf} gridType="A" let:item>
+ <ProductMediaPhoneItem {item} {hasPortraitMedia} {mediaType} />
+ </ShelfItemLayout>
+ {/if}
+
+ {#if appPlatform && shouldShowPlatform}
+ <div class="platform-description">
+ <div class="icon" aria-hidden="true">
+ <SFSymbol name={platformToIconNameMap[appPlatform]} />
+ </div>
+ <div class="platform-label">
+ {$i18n.t(platformToDescriptionMap[appPlatform])}
+ </div>
+ </div>
+ {/if}
+</ShelfWrapper>
+
+{#if shelf.expandedMedia && allPlatforms && allPlatforms.length > 1}
+ <div class="expanded-media">
+ {#if !shouldDisplayExpandedMedia}
+ <button
+ class="expanded-media-header"
+ on:click={displayExpandedMedia}
+ >
+ <div class="all-platforms">
+ <div class="all-platforms-icons">
+ {#each allPlatforms as platform}
+ <div class="icon" aria-hidden="true">
+ <SFSymbol
+ name={platformToIconNameMap[
+ platform.appPlatform
+ ]}
+ />
+ </div>
+ {/each}
+ </div>
+ <div class="all-platforms-names">
+ {allPlatformsDescription}
+ </div>
+ </div>
+ <div class="chevron-container icon" aria-hidden="true">
+ <ChevronDown />
+ </div>
+ </button>
+ {/if}
+ {#if shouldDisplayExpandedMedia}
+ <div class="expanded-media-content" transition:slide>
+ {#each shelf.expandedMedia as expandedMediaShelf}
+ <svelte:self
+ shelf={expandedMediaShelf}
+ isExpandedMedia={true}
+ />
+ {/each}
+ </div>
+ {/if}
+ </div>
+{/if}
+
+{#if !isExpandedMedia}
+ <div class="divider" />
+{/if}
+
+<style>
+ .expanded-media {
+ margin: 15px 0;
+ }
+
+ .expanded-media-header {
+ width: 100%;
+ padding-inline: var(--bodyGutter);
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .platform-description {
+ display: inline-flex;
+ align-items: center;
+ font: var(--body-reduced-semibold);
+ color: var(--systemSecondary);
+ margin-top: 15px;
+ gap: 10px;
+ margin-inline: var(--bodyGutter);
+ }
+
+ .all-platforms {
+ display: inline-flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ color: var(--systemSecondary);
+ }
+
+ .all-platforms-icons {
+ display: inline-flex;
+ gap: 10px;
+ }
+
+ .all-platforms-names {
+ font: var(--body-reduced-semibold);
+ }
+
+ .icon :global(svg) {
+ overflow: visible;
+ height: 16px;
+ max-width: 25px;
+ fill: var(--systemSecondary);
+ position: relative;
+ display: flex;
+ }
+
+ .divider {
+ margin: 10px var(--bodyGutter);
+ border-bottom: 1px solid var(--systemGray4);
+ }
+
+ .chevron-container {
+ top: 2px;
+ }
+
+ .expanded-media-content :global(.shelf:last-of-type) {
+ padding-bottom: 0;
+ }
+
+ .expanded-media-header .all-platforms,
+ .expanded-media-header .chevron-container :global(svg) {
+ transition-duration: 210ms;
+ transition-timing-function: ease-out;
+ transition-property: color, fill;
+ }
+
+ .expanded-media-header:hover .all-platforms,
+ .expanded-media-header:hover .chevron-container :global(svg) {
+ color: var(--systemPrimary);
+ fill: var(--systemPrimary);
+ }
+</style>
diff --git a/src/components/jet/shelf/ProductPageLinkShelf.svelte b/src/components/jet/shelf/ProductPageLinkShelf.svelte
new file mode 100644
index 0000000..7b41e80
--- /dev/null
+++ b/src/components/jet/shelf/ProductPageLinkShelf.svelte
@@ -0,0 +1,59 @@
+<script lang="ts" context="module">
+ import type { Shelf, ProductPageLink } from '@jet-app/app-store/api/models';
+
+ interface ProductPageLinkShelf extends Shelf {
+ items: ProductPageLink[];
+ }
+
+ export function isProductPageLinkShelf(
+ shelf: Shelf,
+ ): shelf is ProductPageLinkShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'productPageLink' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ProductPageLinkItem from '~/components/jet/item/ProductPageLinkItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: ProductPageLinkShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <div class="product-page-link-shelf">
+ {#each shelf.items as item}
+ <li class="product-page-link-item">
+ <ProductPageLinkItem {item} />
+ </li>
+ {/each}
+ </div>
+</ShelfWrapper>
+
+<style lang="scss">
+ $product-page-link-border: 1px solid var(--systemGray4);
+
+ .product-page-link-shelf {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ column-gap: 20px;
+
+ @media (--range-xsmall-down) {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ }
+
+ @media (--range-xsmall-down) {
+ .product-page-link-item:first-child {
+ border-top: $product-page-link-border;
+ }
+
+ .product-page-link-item {
+ width: 100%;
+ border-bottom: $product-page-link-border;
+ padding: 0 var(--bodyGutter);
+ }
+ }
+</style>
diff --git a/src/components/jet/shelf/ProductRatingsShelf.svelte b/src/components/jet/shelf/ProductRatingsShelf.svelte
new file mode 100644
index 0000000..8f09ab5
--- /dev/null
+++ b/src/components/jet/shelf/ProductRatingsShelf.svelte
@@ -0,0 +1,29 @@
+<script lang="ts" context="module">
+ import { type Ratings, type Shelf } from '@jet-app/app-store/api/models';
+
+ interface ProductRatingsShelf extends Shelf {
+ items: Ratings[];
+ }
+
+ export function isProductRatingsShelf(
+ shelf: Shelf,
+ ): shelf is ProductRatingsShelf {
+ let { contentType, items } = shelf;
+
+ return contentType === 'productRatings' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ProductRatingsItem from '~/components/jet/item/ProductRatingsItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: ProductRatingsShelf;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <ShelfItemLayout {shelf} gridType="A" let:item>
+ <ProductRatingsItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/ProductReviewShelf.svelte b/src/components/jet/shelf/ProductReviewShelf.svelte
new file mode 100644
index 0000000..6bc4ecb
--- /dev/null
+++ b/src/components/jet/shelf/ProductReviewShelf.svelte
@@ -0,0 +1,38 @@
+<script lang="ts" context="module">
+ import type { ProductReview, Shelf } from '@jet-app/app-store/api/models';
+
+ interface ProductReviewShelf extends Shelf {
+ items: ProductReview[];
+ }
+
+ export function isProductReviewShelf(
+ shelf: Shelf,
+ ): shelf is ProductReviewShelf {
+ let { contentType, items } = shelf;
+
+ return contentType === 'productReview' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import EditorsChoiceReviewItem, {
+ isEditorsChoiceReviewItem,
+ } from '~/components/jet/item/ProductReview/EditorsChoiceReviewItem.svelte';
+ import UserReviewItem, {
+ isUserReviewItem,
+ } from '~/components/jet/item/ProductReview/UserReviewItem.svelte';
+
+ export let shelf: ProductReviewShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="A" let:item>
+ {#if isUserReviewItem(item)}
+ <UserReviewItem {item} />
+ {:else if isEditorsChoiceReviewItem(item)}
+ <EditorsChoiceReviewItem {item} />
+ {/if}
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/QuoteShelf.svelte b/src/components/jet/shelf/QuoteShelf.svelte
new file mode 100644
index 0000000..3a14f4f
--- /dev/null
+++ b/src/components/jet/shelf/QuoteShelf.svelte
@@ -0,0 +1,80 @@
+<script lang="ts" context="module">
+ import type { Quote, Shelf } from '@jet-app/app-store/api/models';
+
+ interface QuoteShelf extends Shelf {
+ contentType: 'quote';
+ items: [Quote];
+ }
+
+ export function isQuoteShelf(shelf: Shelf): shelf is QuoteShelf {
+ return shelf.contentType === 'quote' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: QuoteShelf;
+
+ $: item = shelf.items[0];
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false}>
+ <div class="outer">
+ <div class="inner">
+ <blockquote>
+ {item.text}
+ </blockquote>
+ <span>{item.credit}</span>
+ </div>
+ </div>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/app/core/globalvars' as *;
+ @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
+ @use 'ac-sasskit/core/locale' as *;
+
+ .outer {
+ display: flex;
+ margin-bottom: 24px;
+ padding: 0 var(--bodyGutter);
+ gap: 6px;
+ }
+
+ .outer::before {
+ content: '❝';
+ font-size: 40px;
+ line-height: 2.2rem;
+ color: var(--systemSecondary);
+
+ @include rtl {
+ content: '❞';
+ }
+ }
+
+ .inner {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ blockquote {
+ font: var(--large-title-emphasized);
+ text-wrap: pretty;
+ }
+
+ blockquote::after {
+ content: '❞';
+ color: var(--systemSecondary);
+
+ @include rtl {
+ content: '❝';
+ }
+ }
+
+ span {
+ font: var(--title-3);
+ color: var(--systemSecondary);
+ }
+</style>
diff --git a/src/components/jet/shelf/ReviewsContainerShelf.svelte b/src/components/jet/shelf/ReviewsContainerShelf.svelte
new file mode 100644
index 0000000..a55fe40
--- /dev/null
+++ b/src/components/jet/shelf/ReviewsContainerShelf.svelte
@@ -0,0 +1,84 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ ReviewsContainer,
+ } from '@jet-app/app-store/api/models';
+
+ export interface ReviewsContainerShelf extends Shelf {
+ items: [ReviewsContainer];
+ }
+
+ export function isReviewsContainerShelf(
+ shelf: Shelf,
+ ): shelf is ReviewsContainerShelf {
+ return (
+ shelf.contentType === 'reviewsContainer' &&
+ Array.isArray(shelf.items)
+ );
+ }
+</script>
+
+<script lang="ts">
+ import RatingComponent from '@amp/web-app-components/src/components/Rating/Rating.svelte';
+
+ import LinkWrapper from '~/components/LinkWrapper.svelte';
+ import ShelfTitle from '~/components/Shelf/Title.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import Grid from '~/components/Grid.svelte';
+ import { getI18n } from '~/stores/i18n';
+ import { getJet } from '~/jet/svelte';
+
+ const i18n = getI18n();
+ const jet = getJet();
+
+ export let shelf: ReviewsContainerShelf;
+
+ $: reviewsContainer = shelf.items[0];
+ $: ({ productAction, ratings } = reviewsContainer);
+
+ $: numberOfRatings = jet.localization.formattedCount(
+ ratings.totalNumberOfRatings,
+ );
+</script>
+
+<ShelfWrapper {shelf}>
+ <header slot="title">
+ {#if productAction}
+ <div class="product-action">
+ <LinkWrapper action={productAction}>
+ {productAction.title}
+ </LinkWrapper>
+ </div>
+ {/if}
+
+ <ShelfTitle title={shelf.title ?? ''} />
+
+ <Grid gridType="A" items={[1]}>
+ <div class="rating">
+ <RatingComponent
+ averageRating={ratings.ratingAverage}
+ ratingCount={ratings.totalNumberOfRatings}
+ ratingCountText={$i18n.t(
+ 'ASE.Web.AppStore.Ratings.CountText',
+ {
+ numberOfRatings,
+ },
+ )}
+ ratingCountsList={ratings.ratingCounts}
+ totalText={$i18n.t('ASE.Web.AppStore.Ratings.TotalText')}
+ />
+ </div>
+ </Grid>
+ </header>
+</ShelfWrapper>
+
+<style>
+ .product-action {
+ --linkColor: var(--keyColor);
+ margin: 0 var(--bodyGutter) 6px;
+ }
+
+ .rating {
+ --ratingBarColor: var(--systemPrimary);
+ }
+</style>
diff --git a/src/components/jet/shelf/ReviewsShelf.svelte b/src/components/jet/shelf/ReviewsShelf.svelte
new file mode 100644
index 0000000..8304444
--- /dev/null
+++ b/src/components/jet/shelf/ReviewsShelf.svelte
@@ -0,0 +1,28 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ Review as ReviewModel,
+ } from '@jet-app/app-store/api/models';
+
+ export interface ReviewsShelf extends Shelf {
+ items: ReviewModel[];
+ }
+
+ export function isReviewsShelf(shelf: Shelf): shelf is ReviewsShelf {
+ return shelf.contentType === 'reviews' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import ReviewItem from '~/components/jet/item/ReviewItem.svelte';
+
+ export let shelf: ReviewsShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="A" let:item>
+ <ReviewItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/RibbonBarShelf.svelte b/src/components/jet/shelf/RibbonBarShelf.svelte
new file mode 100644
index 0000000..44a8ae9
--- /dev/null
+++ b/src/components/jet/shelf/RibbonBarShelf.svelte
@@ -0,0 +1,135 @@
+<script lang="ts" context="module">
+ import type { Shelf, RibbonBarItem } from '@jet-app/app-store/api/models';
+
+ interface RibbonBarShelf extends Shelf {
+ items: RibbonBarItem[];
+ }
+
+ export function isRibbonBarShelf(shelf: Shelf): shelf is RibbonBarShelf {
+ return shelf.contentType === 'ribbonBar' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import Artwork, { getNaturalProfile } from '~/components/Artwork.svelte';
+ import LinkWrapper from '~/components/LinkWrapper.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import SystemImage, {
+ isSystemImageArtwork,
+ } from '~/components/SystemImage.svelte';
+
+ export let shelf: RibbonBarShelf;
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={false} withPaddingTop={false}>
+ <div class="scroll">
+ <ul>
+ {#each shelf.items as ribbonBarItem}
+ {@const action = ribbonBarItem.clickAction}
+ {@const artwork = ribbonBarItem.artwork}
+ {@const title = ribbonBarItem.title}
+ <li>
+ <LinkWrapper {action}>
+ {#if artwork}
+ <div
+ class="artwork-container"
+ style:--aspect-ratio={artwork.width /
+ artwork.height}
+ >
+ {#if isSystemImageArtwork(artwork)}
+ <SystemImage {artwork} />
+ {:else}
+ <Artwork
+ {artwork}
+ profile={getNaturalProfile(artwork, [
+ 17,
+ ])}
+ hasTransparentBackground
+ />
+ {/if}
+ </div>
+ {/if}
+ {title}
+ </LinkWrapper>
+ </li>
+ {/each}
+ </ul>
+ </div>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
+ @use 'ac-sasskit/core/helpers' as *;
+ @use 'ac-sasskit/core/locale' as *;
+
+ .scroll {
+ --gradient-direction: 90deg;
+ overflow-x: auto;
+ scrollbar-width: none;
+ padding-inline-start: var(--bodyGutter);
+ margin-inline-end: var(--bodyGutter);
+ // A small gradient that fades out the ribbon, to indicate that there is more
+ mask-image: linear-gradient(
+ var(--gradient-direction),
+ black calc(100% - 8px),
+ transparent 100%
+ );
+
+ @include rtl {
+ --gradient-direction: -90deg;
+ }
+ }
+
+ ul {
+ font: var(--body-emphasized);
+ display: flex;
+ gap: 4px;
+ padding-bottom: 16px;
+ padding-top: 13px;
+ }
+
+ li {
+ display: flex;
+ margin-inline-end: 8px;
+ flex-shrink: 0;
+ }
+
+ li:last-of-type {
+ padding-inline-end: 8px;
+ }
+
+ li :global(a) {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+ background: var(--pageBG);
+ border-radius: var(--global-border-radius-small);
+ padding: 6px 10px;
+
+ &::after {
+ content: '';
+ display: block;
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-radius: var(--global-border-radius-small);
+ box-shadow: var(--shadow-small);
+ z-index: calc(var(--z-default) - 1);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ background: var(--systemGray5-default_IC);
+ }
+ }
+
+ .artwork-container {
+ --artwork-override-height: 17px;
+ flex-shrink: 0;
+ aspect-ratio: var(--aspect-ratio);
+ height: 17px;
+ }
+</style>
diff --git a/src/components/jet/shelf/SearchLinkShelf.svelte b/src/components/jet/shelf/SearchLinkShelf.svelte
new file mode 100644
index 0000000..6b29780
--- /dev/null
+++ b/src/components/jet/shelf/SearchLinkShelf.svelte
@@ -0,0 +1,26 @@
+<script lang="ts" context="module">
+ import type { Shelf, SearchLink } from '@jet-app/app-store/api/models';
+
+ interface SearchLinkShelf extends Shelf {
+ items: SearchLink[];
+ }
+
+ export function isSearchLinkShelf(shelf: Shelf): shelf is SearchLinkShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'searchLink' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import SearchLinkItem from '~/components/jet/item/SearchLinkItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: SearchLinkShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="SearchLink" let:item>
+ <SearchLinkItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/SearchResultShelf.svelte b/src/components/jet/shelf/SearchResultShelf.svelte
new file mode 100644
index 0000000..9c15d3e
--- /dev/null
+++ b/src/components/jet/shelf/SearchResultShelf.svelte
@@ -0,0 +1,49 @@
+<script lang="ts" context="module">
+ import type {
+ AppSearchResult,
+ Shelf,
+ SearchResult,
+ AppEventSearchResult,
+ } from '@jet-app/app-store/api/models';
+
+ import AppSearchResultItem, {
+ isAppSearchResult,
+ isAppEventSearchResult,
+ } from '~/components/jet/item/SearchResult/AppSearchResultItem.svelte';
+
+ /**
+ * All sub-classes of {@linkcode SearchResult} that this component can handle rendering
+ */
+ type RenderableSearchResult = AppSearchResult | AppEventSearchResult;
+
+ interface SearchResultShelf extends Shelf {
+ items: SearchResult[];
+ }
+
+ export function isSearchResultShelf(
+ shelf: Shelf,
+ ): shelf is SearchResultShelf {
+ return (
+ shelf.contentType === 'searchResult' && Array.isArray(shelf.items)
+ );
+ }
+
+ export function isRenderableInSearchResultsShelf(
+ item: SearchResult,
+ ): item is RenderableSearchResult {
+ return isAppSearchResult(item) || isAppEventSearchResult(item);
+ }
+</script>
+
+<script lang="ts">
+ import Grid from '~/components/Grid.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: SearchResultShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <Grid gridType="SearchResult" items={shelf.items} let:item>
+ <AppSearchResultItem {item} />
+ </Grid>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/Shelf.svelte b/src/components/jet/shelf/Shelf.svelte
new file mode 100644
index 0000000..6cbb0f6
--- /dev/null
+++ b/src/components/jet/shelf/Shelf.svelte
@@ -0,0 +1,320 @@
+<script lang="ts">
+ import type { Shelf } from '@jet-app/app-store/api/models';
+
+ // Components for specific types of `Shelf`
+ import AccessibilityHeaderShelf, {
+ isAccessibilityHeaderShelf,
+ } from '~/components/jet/shelf/AccessibilityHeaderShelf.svelte';
+ import AccessibilityFeaturesShelf, {
+ isAccessibilityFeaturesShelf,
+ } from '~/components/jet/shelf/AccessibilityFeaturesShelf.svelte';
+ import AccessibilityDeveloperLinkShelf, {
+ isAccessibilityDeveloperLinkShelf,
+ } from './AccessibilityDeveloperLinkShelf.svelte';
+ import ActionShelf, {
+ isActionShelf,
+ } from '~/components/jet/shelf/ActionShelf.svelte';
+ import AnnotationShelf, {
+ isAnnotationShelf,
+ } from '~/components/jet/shelf/AnnotationShelf.svelte';
+ import AppEventDetailShelf, {
+ isAppEventDetailShelf,
+ } from '~/components/jet/shelf/AppEventDetailShelf.svelte';
+ import AppPromotionShelf, {
+ isAppPromotionShelf,
+ } from '~/components/jet/shelf/AppPromotionShelf.svelte';
+ import AppShowcaseShelf, {
+ isAppShowcaseShelf,
+ } from '~/components/jet/shelf/AppShowcaseShelf.svelte';
+ import AppTrailerLockupShelf, {
+ isAppTrailerLockupShelf,
+ } from '~/components/jet/shelf/AppTrailerLockupShelf.svelte';
+ import ArcadeFooterShelf, {
+ isArcadeFooterShelf,
+ } from '~/components/jet/shelf/ArcadeFooterShelf.svelte';
+ import { isBannerShelf } from '~/components/jet/shelf/BannerShelf.svelte';
+ import BrickShelf, {
+ isBrickShelf,
+ } from '~/components/jet/shelf/BrickShelf.svelte';
+ import CategoryBrickShelf, {
+ isCategoryBrickShelf,
+ } from '~/components/jet/shelf/CategoryBrickShelf.svelte';
+ import EditorialCardShelf, {
+ isEditorialCardShelf,
+ } from '~/components/jet/shelf/EditorialCardShelf.svelte';
+ import EditorialLinkShelf, {
+ isEditorialLinkShelf,
+ } from '~/components/jet/shelf/EditorialLinkShelf.svelte';
+ import FramedArtworkShelf, {
+ isFramedArtworkShelf,
+ } from '~/components/jet/shelf/FramedArtworkShelf.svelte';
+ import FramedVideoShelf, {
+ isFramedVideoShelf,
+ } from '~/components/jet/shelf/FramedVideoShelf.svelte';
+ import HeroCarouselShelf, {
+ isHeroCarouselShelf,
+ } from '~/components/jet/shelf/HeroCarouselShelf.svelte';
+ import HorizontalRuleShelf, {
+ isHorizontalRuleShelf,
+ } from '~/components/jet/shelf/HorizontalRuleShelf.svelte';
+ import InAppPurchaseLockupShelf, {
+ isInAppPurchaseLockupShelf,
+ } from '~/components/jet/shelf/InAppPurchaseLockupShelf.svelte';
+ import LargeHeroBreakoutShelf, {
+ isLargeHeroBreakoutShelf,
+ } from '~/components/jet/shelf/LargeHeroBreakoutShelf.svelte';
+ import LargeBrickShelf, {
+ isLargeBrickShelf,
+ } from '~/components/jet/shelf/LargeBrickShelf.svelte';
+ import LargeImageLockupShelf, {
+ isLargeImageLockupShelf,
+ } from '~/components/jet/shelf/LargeImageLockupShelf.svelte';
+ import LargeLockupShelf, {
+ isLargeLockupShelf,
+ } from '~/components/jet/shelf/LargeLockupShelf.svelte';
+ import LargeStoryCardShelf, {
+ isLargeStoryCardShelf,
+ } from '~/components/jet/shelf/LargeStoryCardShelf.svelte';
+ import LinkableTextShelf, {
+ isLinkableTextShelf,
+ } from '~/components/jet/shelf/LinkableTextShelf.svelte';
+ import {
+ isMarkerShelf,
+ type MarkerShelf as MarkerShelfModel,
+ } from '~/components/jet/shelf/MarkerShelf.svelte';
+ import MediumImageLockupShelf, {
+ isMediumImageLockupShelf,
+ } from '~/components/jet/shelf/MediumImageLockupShelf.svelte';
+ import MediumLockupShelf, {
+ isMediumLockupShelf,
+ } from '~/components/jet/shelf/MediumLockupShelf.svelte';
+ import MediumStoryCardShelf, {
+ isMediumStoryCardShelf,
+ } from '~/components/jet/shelf/MediumStoryCardShelf.svelte';
+ import ProductBadgeShelf, {
+ isProductBadgeShelf,
+ } from '~/components/jet/shelf/ProductBadgeShelf.svelte';
+ import PageHeaderShelf, {
+ isPageHeaderShelf,
+ } from '~/components/jet/shelf/PageHeaderShelf.svelte';
+ import ParagraphShelf, {
+ isParagraphShelf,
+ } from '~/components/jet/shelf/ParagraphShelf.svelte';
+ import PosterLockupShelf, {
+ isPosterLockupShelf,
+ } from '~/components/jet/shelf/PosterLockupShelf.svelte';
+ import ProductMediaShelf, {
+ isProductMediaShelf,
+ } from '~/components/jet/shelf/ProductMediaShelf.svelte';
+ import ProductDescriptionShelf, {
+ isProductDescriptionShelf,
+ } from '~/components/jet/shelf/ProductDescriptionShelf.svelte';
+ import ProductRatingsShelf, {
+ isProductRatingsShelf,
+ } from '~/components/jet/shelf/ProductRatingsShelf.svelte';
+ import ProductReviewShelf, {
+ isProductReviewShelf,
+ } from '~/components/jet/shelf/ProductReviewShelf.svelte';
+ import RibbonBarShelf, {
+ isRibbonBarShelf,
+ } from '~/components/jet/shelf/RibbonBarShelf.svelte';
+ import SearchLinkShelf, {
+ isSearchLinkShelf,
+ } from '~/components/jet/shelf/SearchLinkShelf.svelte';
+ import SearchResultShelf, {
+ isSearchResultShelf,
+ } from '~/components/jet/shelf/SearchResultShelf.svelte';
+ import SmallBreakoutShelf, {
+ isSmallBreakoutShelf,
+ } from '~/components/jet/shelf/SmallBreakoutShelf.svelte';
+ import SmallBrickShelf, {
+ isSmallBrickShelf,
+ } from '~/components/jet/shelf/SmallBrickShelf.svelte';
+ import SmallLockupShelf, {
+ isSmallLockupShelf,
+ } from '~/components/jet/shelf/SmallLockupShelf.svelte';
+ import SmallStoryCardShelf, {
+ isSmallStoryCardShelf,
+ } from '~/components/jet/shelf/SmallStoryCardShelf.svelte';
+ import PrivacyHeaderShelf, {
+ isPrivacyHeaderShelf,
+ } from '~/components/jet/shelf/PrivacyHeaderShelf.svelte';
+ import PrivacyFooterShelf, {
+ isPrivacyFooterShelf,
+ } from '~/components/jet/shelf/PrivacyFooterShelf.svelte';
+ import PrivacyTypeShelf, {
+ isPrivacyTypeShelf,
+ } from '~/components/jet/shelf/PrivacyTypeShelf.svelte';
+ import ProductCapabilityShelf, {
+ isProductCapabilityShelf,
+ } from '~/components/jet/shelf/ProductCapabilityShelf.svelte';
+ import ProductPageLinkShelf, {
+ isProductPageLinkShelf,
+ } from './ProductPageLinkShelf.svelte';
+ import QuoteShelf, {
+ isQuoteShelf,
+ } from '~/components/jet/shelf/QuoteShelf.svelte';
+ import ReviewsContainerShelf, {
+ isReviewsContainerShelf,
+ } from '~/components/jet/shelf/ReviewsContainerShelf.svelte';
+ import ReviewsShelf, {
+ isReviewsShelf,
+ } from '~/components/jet/shelf/ReviewsShelf.svelte';
+ import TitledParagraphShelf, {
+ isTitledParagraphShelf,
+ } from '~/components/jet/shelf/TitledParagraphShelf.svelte';
+ import TodayCardShelf, {
+ isTodayCardShelf,
+ } from '~/components/jet/shelf/TodayCardShelf.svelte';
+ import UberShelf, {
+ isUberShelf,
+ } from '~/components/jet/shelf//UberShelf.svelte';
+ import FallbackShelf, {
+ isFallbackShelf,
+ } from '~/components/jet/shelf/FallbackShelf.svelte';
+
+ interface $$Slots {
+ /**
+ * If the `shelf` is recognized to be a {@linkcode MarkerShelfModel}, this
+ * slot is rendered with the `shelf` as data rather than rendering the
+ * shelf directly.
+ *
+ * This is done because "marker" shelves need the whole "page" definition to
+ * be rendered, which is not available at this level of the UI. Rather than
+ * having to pass that data down to this level, we yield rendering back to
+ * the "parent" component that can provide that data directly.
+ */
+ 'marker-shelf': {
+ shelf: MarkerShelfModel;
+ };
+ }
+
+ export let shelf: Shelf;
+</script>
+
+<!--
+@component
+Render a generic `Shelf`
+
+This component is responsible for rendering any kind of `Shelf` that
+the App Store is capable of rendering. It primarily does this by trying
+to narrow the generic `Shelf` down to a more-specific type and then
+rendering a component specifically made for it
+-->
+
+{#if isAccessibilityHeaderShelf(shelf)}
+ <AccessibilityHeaderShelf {shelf} />
+{:else if isAccessibilityFeaturesShelf(shelf)}
+ <AccessibilityFeaturesShelf {shelf} />
+{:else if isAccessibilityDeveloperLinkShelf(shelf)}
+ <AccessibilityDeveloperLinkShelf {shelf} />
+{:else if isActionShelf(shelf)}
+ <ActionShelf {shelf} />
+{:else if isAnnotationShelf(shelf)}
+ <AnnotationShelf {shelf} />
+{:else if isAppEventDetailShelf(shelf)}
+ <AppEventDetailShelf {shelf} />
+{:else if isAppPromotionShelf(shelf)}
+ <AppPromotionShelf {shelf} />
+{:else if isAppShowcaseShelf(shelf)}
+ <AppShowcaseShelf {shelf} />
+{:else if isAppTrailerLockupShelf(shelf)}
+ <AppTrailerLockupShelf {shelf} />
+{:else if isArcadeFooterShelf(shelf)}
+ <ArcadeFooterShelf {shelf} />
+{:else if isBannerShelf(shelf)}
+ <!-- a no-op until we determine if we actually want to support these banners -->
+ <!-- <BannerShelf {shelf} /> -->
+{:else if isBrickShelf(shelf)}
+ <BrickShelf {shelf} />
+{:else if isCategoryBrickShelf(shelf)}
+ <CategoryBrickShelf {shelf} />
+{:else if isEditorialCardShelf(shelf)}
+ <EditorialCardShelf {shelf} />
+{:else if isEditorialLinkShelf(shelf)}
+ <EditorialLinkShelf {shelf} />
+{:else if isFramedArtworkShelf(shelf)}
+ <FramedArtworkShelf {shelf} />
+{:else if isFramedVideoShelf(shelf)}
+ <FramedVideoShelf {shelf} />
+{:else if isHeroCarouselShelf(shelf)}
+ <HeroCarouselShelf {shelf} />
+{:else if isHorizontalRuleShelf(shelf)}
+ <HorizontalRuleShelf {shelf} />
+{:else if isInAppPurchaseLockupShelf(shelf)}
+ <InAppPurchaseLockupShelf {shelf} />
+{:else if isLargeHeroBreakoutShelf(shelf)}
+ <LargeHeroBreakoutShelf {shelf} />
+{:else if isLargeBrickShelf(shelf)}
+ <LargeBrickShelf {shelf} />
+{:else if isLargeImageLockupShelf(shelf)}
+ <LargeImageLockupShelf {shelf} />
+{:else if isLargeLockupShelf(shelf)}
+ <LargeLockupShelf {shelf} />
+{:else if isLargeStoryCardShelf(shelf)}
+ <LargeStoryCardShelf {shelf} />
+{:else if isLinkableTextShelf(shelf)}
+ <LinkableTextShelf {shelf} />
+{:else if isProductDescriptionShelf(shelf)}
+ <ProductDescriptionShelf {shelf} />
+{:else if isMediumImageLockupShelf(shelf)}
+ <MediumImageLockupShelf {shelf} />
+{:else if isMediumLockupShelf(shelf)}
+ <MediumLockupShelf {shelf} />
+{:else if isMediumStoryCardShelf(shelf)}
+ <MediumStoryCardShelf {shelf} />
+{:else if isPosterLockupShelf(shelf)}
+ <PosterLockupShelf {shelf} />
+{:else if isProductBadgeShelf(shelf)}
+ <ProductBadgeShelf {shelf} />
+{:else if isPageHeaderShelf(shelf)}
+ <PageHeaderShelf {shelf} />
+{:else if isParagraphShelf(shelf)}
+ <ParagraphShelf {shelf} />
+{:else if isPrivacyHeaderShelf(shelf)}
+ <PrivacyHeaderShelf {shelf} />
+{:else if isPrivacyFooterShelf(shelf)}
+ <PrivacyFooterShelf {shelf} />
+{:else if isPrivacyTypeShelf(shelf)}
+ <PrivacyTypeShelf {shelf} />
+{:else if isProductMediaShelf(shelf)}
+ <ProductMediaShelf {shelf} />
+{:else if isProductRatingsShelf(shelf)}
+ <ProductRatingsShelf {shelf} />
+{:else if isProductReviewShelf(shelf)}
+ <ProductReviewShelf {shelf} />
+{:else if isRibbonBarShelf(shelf)}
+ <RibbonBarShelf {shelf} />
+{:else if isSearchLinkShelf(shelf)}
+ <SearchLinkShelf {shelf} />
+{:else if isSearchResultShelf(shelf)}
+ <SearchResultShelf {shelf} />
+{:else if isSmallBreakoutShelf(shelf)}
+ <SmallBreakoutShelf {shelf} />
+{:else if isSmallBrickShelf(shelf)}
+ <SmallBrickShelf {shelf} />
+{:else if isSmallStoryCardShelf(shelf)}
+ <SmallStoryCardShelf {shelf} />
+{:else if isSmallLockupShelf(shelf)}
+ <SmallLockupShelf {shelf} />
+{:else if isProductCapabilityShelf(shelf)}
+ <ProductCapabilityShelf {shelf} />
+{:else if isProductPageLinkShelf(shelf)}
+ <ProductPageLinkShelf {shelf} />
+{:else if isQuoteShelf(shelf)}
+ <QuoteShelf {shelf} />
+{:else if isReviewsContainerShelf(shelf)}
+ <ReviewsContainerShelf {shelf} />
+{:else if isReviewsShelf(shelf)}
+ <ReviewsShelf {shelf} />
+{:else if isTodayCardShelf(shelf)}
+ <TodayCardShelf {shelf} />
+{:else if isTitledParagraphShelf(shelf)}
+ <TitledParagraphShelf {shelf} />
+{:else if isUberShelf(shelf)}
+ <UberShelf {shelf} />
+{:else if isMarkerShelf(shelf)}
+ <slot name="marker-shelf" {shelf} />
+{:else if isFallbackShelf(shelf)}
+ <FallbackShelf {shelf} />
+{/if}
diff --git a/src/components/jet/shelf/SmallBreakoutShelf.svelte b/src/components/jet/shelf/SmallBreakoutShelf.svelte
new file mode 100644
index 0000000..095cf7f
--- /dev/null
+++ b/src/components/jet/shelf/SmallBreakoutShelf.svelte
@@ -0,0 +1,32 @@
+<script lang="ts" context="module">
+ import type {
+ LargeHeroBreakout,
+ Shelf,
+ SmallBreakout,
+ } from '@jet-app/app-store/api/models';
+
+ interface SmallBreakoutShelf extends Shelf {
+ items: SmallBreakout[];
+ }
+
+ export function isSmallBreakoutShelf(
+ shelf: Shelf,
+ ): shelf is SmallBreakoutShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'smallBreakout' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import SmallBreakoutItem from '~/components/jet/item/SmallBreakoutItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: SmallBreakoutShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="Spotlight" let:item>
+ <SmallBreakoutItem {item} />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/SmallBrickShelf.svelte b/src/components/jet/shelf/SmallBrickShelf.svelte
new file mode 100644
index 0000000..34426cf
--- /dev/null
+++ b/src/components/jet/shelf/SmallBrickShelf.svelte
@@ -0,0 +1,26 @@
+<script lang="ts" context="module">
+ import type { Brick, Shelf } from '@jet-app/app-store/api/models';
+
+ interface SmallBrickShelf extends Shelf {
+ items: Brick[];
+ }
+
+ export function isSmallBrickShelf(shelf: Shelf): shelf is SmallBrickShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'smallBrick' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import BrickItem from '~/components/jet/item/BrickItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: SmallBrickShelf;
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout {shelf} gridType="C" let:item>
+ <BrickItem {item} shouldOverlayDescription />
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/SmallLockupShelf.svelte b/src/components/jet/shelf/SmallLockupShelf.svelte
new file mode 100644
index 0000000..e286671
--- /dev/null
+++ b/src/components/jet/shelf/SmallLockupShelf.svelte
@@ -0,0 +1,54 @@
+<script lang="ts" context="module">
+ import type { Lockup, Shelf } from '@jet-app/app-store/api/models';
+
+ interface SmallLockupShelf extends Shelf {
+ items: Lockup[];
+ }
+
+ export function isSmallLockupShelf(
+ shelf: Shelf,
+ ): shelf is SmallLockupShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'smallLockup' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+ import SmallLockupItem from '~/components/jet/item/SmallLockupItem.svelte';
+ import SmallLockupWithOrdinalItem, {
+ isSmallLockupWithOrdinalItem,
+ } from '~/components/jet/item/SmallLockupWithOrdinalItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: SmallLockupShelf;
+
+ $: ({ isArticleContext = false } = shelf.presentationHints ?? {});
+ $: itemHasOrdinal = shelf.items.some((item) => item.ordinal);
+ $: gridType = (() => {
+ if (itemHasOrdinal) {
+ return 'SmallLockupWithOrdinal';
+ }
+
+ if (isArticleContext) {
+ return 'Spotlight';
+ }
+
+ return 'SmallLockup';
+ })();
+</script>
+
+<ShelfWrapper {shelf}>
+ <ShelfItemLayout
+ {shelf}
+ {gridType}
+ rowsPerColumnOverride={gridType === 'SmallLockup' ? 3 : null}
+ let:item
+ >
+ {#if isSmallLockupWithOrdinalItem(item)}
+ <SmallLockupWithOrdinalItem {item} />
+ {:else}
+ <SmallLockupItem {item} --margin-inline-end="16px" />
+ {/if}
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/SmallStoryCardShelf.svelte b/src/components/jet/shelf/SmallStoryCardShelf.svelte
new file mode 100644
index 0000000..c1a85ad
--- /dev/null
+++ b/src/components/jet/shelf/SmallStoryCardShelf.svelte
@@ -0,0 +1,66 @@
+<script lang="ts" context="module">
+ import type { Shelf, TodayCard } from '@jet-app/app-store/api/models';
+
+ interface SmallStoryCardShelf extends Shelf {
+ contentType: 'smallStoryCard';
+ items: TodayCard[];
+ }
+
+ export function isSmallStoryCardShelf(
+ shelf: Shelf,
+ ): shelf is SmallStoryCardShelf {
+ const { contentType, items } = shelf;
+ return contentType === 'smallStoryCard' && Array.isArray(items);
+ }
+</script>
+
+<script lang="ts">
+ import SmallStoryCardWithMediaItem, {
+ isSmallStoryCardWithMediaItem,
+ } from '~/components/jet/item/SmallStoryCardWithMediaItem.svelte';
+ import SmallStoryCardWithArtworkItem, {
+ isSmallStoryCardWithArtworkItem,
+ } from '~/components/jet/item/SmallStoryCardWithArtworkItem.svelte';
+ import SmallStoryCardWithMediaRiver, {
+ isSmallStoryCardWithMediaRiver,
+ } from '~/components/jet/item/SmallStoryCardWithMediaRiver.svelte';
+ import SmallStoryCardWithMediaAppIcon, {
+ isSmallStoryCardWithMediaAppIcon,
+ } from '~/components/jet/item/SmallStoryCardWithMediaAppIcon.svelte';
+ import SmallStoryCardMediaBrandedSingleApp, {
+ isSmallStoryCardMediaBrandedSingleApp,
+ } from '~/components/jet/item/SmallStoryCardMediaBrandedSingleApp.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfItemLayout from '~/components/ShelfItemLayout.svelte';
+
+ export let shelf: SmallStoryCardShelf;
+
+ $: ({ isArticleContext = false } = shelf.presentationHints ?? {});
+ $: gridType = (() => {
+ if (isArticleContext) {
+ return 'SmallStoryCard';
+ }
+
+ if (shelf.items.some(isSmallStoryCardWithArtworkItem)) {
+ return 'D';
+ }
+
+ return 'B';
+ })();
+</script>
+
+<ShelfWrapper {shelf} withBottomPadding={!isArticleContext}>
+ <ShelfItemLayout {shelf} {gridType} let:item>
+ {#if isSmallStoryCardWithMediaRiver(item)}
+ <SmallStoryCardWithMediaRiver {item} />
+ {:else if isSmallStoryCardWithMediaAppIcon(item)}
+ <SmallStoryCardWithMediaAppIcon {item} />
+ {:else if isSmallStoryCardMediaBrandedSingleApp(item)}
+ <SmallStoryCardMediaBrandedSingleApp {item} />
+ {:else if isSmallStoryCardWithMediaItem(item)}
+ <SmallStoryCardWithMediaItem {item} />
+ {:else if isSmallStoryCardWithArtworkItem(item)}
+ <SmallStoryCardWithArtworkItem {item} />
+ {/if}
+ </ShelfItemLayout>
+</ShelfWrapper>
diff --git a/src/components/jet/shelf/TitledParagraphShelf.svelte b/src/components/jet/shelf/TitledParagraphShelf.svelte
new file mode 100644
index 0000000..41c1d74
--- /dev/null
+++ b/src/components/jet/shelf/TitledParagraphShelf.svelte
@@ -0,0 +1,118 @@
+<script lang="ts" context="module">
+ import {
+ type Action,
+ type FlowAction,
+ type GenericPage,
+ type Shelf,
+ type TitledParagraph,
+ isFlowAction,
+ } from '@jet-app/app-store/api/models';
+
+ interface TitledParagraphShelf extends Shelf {
+ items: [TitledParagraph];
+ }
+
+ interface VersionHistoryPage extends FlowAction {
+ page: 'versionHistory';
+ pageData: GenericPage;
+ }
+
+ export function isTitledParagraphShelf(
+ shelf: Shelf,
+ ): shelf is TitledParagraphShelf {
+ const { contentType, items } = shelf;
+
+ return contentType === 'titledParagraph' && Array.isArray(items);
+ }
+
+ function isVersionHistoryFlowAction(
+ action: Action,
+ ): action is VersionHistoryPage {
+ return isFlowAction(action) && action.page === 'versionHistory';
+ }
+</script>
+
+<script lang="ts">
+ import { createEventDispatcher, type SvelteComponent } from 'svelte';
+ import Modal from '@amp/web-app-components/src/components/Modal/Modal.svelte';
+ import ContentModal from '~/components/jet/item/ContentModal.svelte';
+ import TitledParagraphItem, {
+ isTitledParagraphItem,
+ } from '~/components/jet/item/TitledParagraphItem.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import ShelfTitle from '~/components/Shelf/Title.svelte';
+ import { getI18n } from '~/stores/i18n';
+ import { getJetPerform } from '~/jet';
+ import { VERSION_HISTORY_MODAL_ID } from '~/utils/metrics';
+
+ const perform = getJetPerform();
+ export let shelf: TitledParagraphShelf;
+
+ let modalComponent: SvelteComponent;
+ let modalTriggerElement: HTMLElement | null = null;
+
+ const { seeAllAction } = shelf;
+ const i18n = getI18n();
+ const translateFn = (key: string) => $i18n.t(key);
+ const handleModalClose = () => modalComponent.close();
+ const handleOpenModalClick = (e: Event) => {
+ modalTriggerElement = e.target as HTMLElement;
+ modalComponent?.showModal();
+ perform(destination);
+ };
+
+ const destination =
+ seeAllAction && isVersionHistoryFlowAction(seeAllAction)
+ ? seeAllAction
+ : undefined;
+
+ const pageData = destination?.pageData;
+</script>
+
+<ShelfWrapper {shelf}>
+ <div slot="title" class="title-container">
+ {#if shelf.title}
+ <button on:click={handleOpenModalClick}>
+ <ShelfTitle title={shelf.title} seeAllAction={destination} />
+ </button>
+ {/if}
+
+ {#if pageData}
+ <Modal {modalTriggerElement} bind:this={modalComponent}>
+ <ContentModal
+ on:close={handleModalClose}
+ title={pageData.title || null}
+ subtitle={null}
+ targetId={VERSION_HISTORY_MODAL_ID}
+ >
+ <svelte:fragment slot="content">
+ <ul>
+ {#each pageData.shelves as shelf}
+ {#each shelf.items || [] as item}
+ {#if isTitledParagraphItem(item)}
+ <li>
+ <TitledParagraphItem {item} />
+ </li>
+ {/if}
+ {/each}
+ {/each}
+ </ul>
+ </svelte:fragment>
+ </ContentModal>
+ </Modal>
+ {/if}
+ </div>
+
+ {#each shelf.items as item}
+ <TitledParagraphItem {item} />
+ {/each}
+</ShelfWrapper>
+
+<style>
+ .title-container {
+ display: flex;
+ justify-content: space-between;
+ padding-top: 16px;
+ padding-inline-end: var(--bodyGutter);
+ }
+</style>
diff --git a/src/components/jet/shelf/TodayCardShelf.svelte b/src/components/jet/shelf/TodayCardShelf.svelte
new file mode 100644
index 0000000..e872112
--- /dev/null
+++ b/src/components/jet/shelf/TodayCardShelf.svelte
@@ -0,0 +1,187 @@
+<script lang="ts" context="module">
+ import type {
+ Shelf,
+ TodayCard as TodayCardModel,
+ } from '@jet-app/app-store/api/models';
+
+ export interface TodayCardShelf extends Shelf {
+ contentType: 'todayCard';
+
+ items: TodayCardModel[];
+ }
+
+ export function isTodayCardShelf(shelf: Shelf): shelf is TodayCardShelf {
+ return shelf.contentType === 'todayCard' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+ import TodayCard from '~/components/jet/today-card/TodayCard.svelte';
+
+ import { getTodayCardLayoutConfiguration } from '~/context/today-card-layout';
+
+ export let shelf: TodayCardShelf;
+
+ $: ({
+ wrap: { shouldStretchFirstCard: shouldStretchFirstCardWrap },
+ nowrap: { shouldStretchFirstCard: shouldStretchFirstCardNoWrap },
+ } = getTodayCardLayoutConfiguration(shelf));
+</script>
+
+<ShelfWrapper {shelf}>
+ <div>
+ <div
+ class="today-card-row"
+ class:today-card-row__stretch-first-wrap={shouldStretchFirstCardWrap &&
+ shelf.items.length >= 2}
+ class:today-card-row__stretch-first-nowrap={shouldStretchFirstCardNoWrap &&
+ shelf.items.length >= 2}
+ class:today-card-row__stretch-last-wrap={!shouldStretchFirstCardWrap &&
+ shelf.items.length >= 2}
+ class:today-card-row__stretch-last-nowrap={!shouldStretchFirstCardNoWrap &&
+ shelf.items.length >= 2}
+ class:today-card-row__1-card={shelf.items.length == 1}
+ class:today-card-row__2-card={shelf.items.length == 2}
+ class:today-card-row__3-card={shelf.items.length == 3}
+ class:today-card-row__4-card={shelf.items.length >= 4}
+ >
+ {#each shelf.items.slice(0, 4) as card}
+ <div class="today-card-wrapper">
+ <TodayCard {card} />
+ </div>
+ {/each}
+ </div>
+ </div>
+</ShelfWrapper>
+
+<style lang="scss">
+ @use '@amp/web-shared-styles/sasskit-stylekit/ac-sasskit-config';
+ @use '@amp/web-shared-styles/app/core/globalvars' as *;
+ @use 'ac-sasskit/modules/viewportcontent/core' as *;
+ @use 'amp/stylekit/core/viewports' as *;
+ @use 'amp/stylekit/core/mixins/browser-targets' as *;
+
+ @mixin stretch-card($flex-shrink: 1) {
+ aspect-ratio: unset;
+ justify-self: stretch;
+ align-self: stretch;
+ width: auto;
+ flex-shrink: $flex-shrink;
+ flex-grow: 1;
+ }
+
+ .today-card-row {
+ --card-default-width: 407px;
+ --card-default-height: 534px;
+ --card-row-gap: 16px;
+ min-width: min(var(--card-default-width), 100vw);
+ padding: 0 25px;
+ display: flex;
+ flex-direction: column;
+ gap: var(--card-row-gap);
+
+ @media (--range-medium-up) {
+ padding: 0 40px;
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+ }
+
+ .today-card-wrapper {
+ --artworkShadowInset: 0;
+ --afterShadowBorderRadius: 0px;
+ aspect-ratio: 3 / 4;
+ width: 100%;
+ flex-shrink: 0;
+ max-height: 600px;
+ min-height: 100px;
+
+ > :global(a) {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
+
+ @include target-safari {
+ @media screen and (760px <= width) {
+ height: 600px;
+ aspect-ratio: unset;
+ }
+ }
+
+ @media (--range-medium-up) {
+ width: auto;
+ height: var(--card-default-height);
+ aspect-ratio: 3 / 4;
+ }
+ }
+
+ @media (--range-medium-up) {
+ .today-card-row__1-card .today-card-wrapper {
+ @include stretch-card;
+ }
+ }
+
+ @media (--range-medium-up) and (--range-large-down) {
+ .today-card-row__2-card {
+ &.today-card-row__stretch-first-wrap
+ .today-card-wrapper:first-child,
+ &.today-card-row__stretch-last-wrap .today-card-wrapper:last-child {
+ @include stretch-card;
+ }
+ }
+
+ .today-card-row__3-card {
+ .today-card-wrapper:first-child {
+ flex-basis: 100%;
+
+ @include stretch-card(0);
+ }
+
+ &.today-card-row__stretch-first-wrap
+ .today-card-wrapper:nth-child(2),
+ &.today-card-row__stretch-last-wrap .today-card-wrapper:last-child {
+ @include stretch-card;
+ }
+ }
+ }
+
+ @media (--range-medium-up) {
+ .today-card-row__4-card {
+ &.today-card-row__stretch-first-wrap
+ .today-card-wrapper:first-child,
+ &.today-card-row__stretch-first-wrap .today-card-wrapper:last-child,
+ &.today-card-row__stretch-last-wrap
+ .today-card-wrapper:nth-child(2),
+ &.today-card-row__stretch-last-wrap
+ .today-card-wrapper:nth-child(3) {
+ flex-basis: calc(
+ 100% - var(--card-default-width) - var(--card-row-gap)
+ );
+
+ @include stretch-card;
+ }
+ }
+ }
+
+ @media (--range-xlarge-up) {
+ .today-card-row__2-card {
+ &.today-card-row__stretch-first-nowrap
+ .today-card-wrapper:first-child,
+ &.today-card-row__stretch-last-nowrap
+ .today-card-wrapper:last-child {
+ @include stretch-card;
+ }
+ }
+
+ .today-card-row__3-card {
+ &.today-card-row__stretch-first-nowrap
+ .today-card-wrapper:first-child,
+ &.today-card-row__stretch-last-nowrap
+ .today-card-wrapper:last-child {
+ @include stretch-card;
+ }
+ }
+ }
+</style>
diff --git a/src/components/jet/shelf/UberShelf.svelte b/src/components/jet/shelf/UberShelf.svelte
new file mode 100644
index 0000000..6cdf004
--- /dev/null
+++ b/src/components/jet/shelf/UberShelf.svelte
@@ -0,0 +1,40 @@
+<script lang="ts" context="module">
+ import type { Shelf, Uber } from '@jet-app/app-store/api/models';
+
+ interface UberShelf extends Shelf {
+ contentType: 'uber';
+ items: [Uber];
+ }
+
+ export function isUberShelf(shelf: Shelf): shelf is UberShelf {
+ return shelf.contentType === 'uber' && Array.isArray(shelf.items);
+ }
+</script>
+
+<script lang="ts">
+ import Artwork from '~/components/Artwork.svelte';
+ import ShelfWrapper from '~/components/Shelf/Wrapper.svelte';
+
+ export let shelf: UberShelf;
+
+ $: uber = shelf.items[0];
+ $: artwork = uber.artwork;
+</script>
+
+{#if artwork}
+ <ShelfWrapper withPaddingTop={false} withBottomPadding={false}>
+ <div class="artwork-container">
+ <Artwork {artwork} profile="uber-shelf" />
+ </div>
+ </ShelfWrapper>
+{/if}
+
+<style>
+ .artwork-container {
+ border-bottom: 1px solid var(--systemQuaternary-onDark);
+
+ @media (--range-xlarge-only) {
+ border: 1px solid var(--systemQuaternary-onDark);
+ }
+ }
+</style>