diff options
Diffstat (limited to 'src/components/jet/item/LargeHeroBreakoutItem.svelte')
| -rw-r--r-- | src/components/jet/item/LargeHeroBreakoutItem.svelte | 268 |
1 files changed, 268 insertions, 0 deletions
diff --git a/src/components/jet/item/LargeHeroBreakoutItem.svelte b/src/components/jet/item/LargeHeroBreakoutItem.svelte new file mode 100644 index 0000000..d07eec8 --- /dev/null +++ b/src/components/jet/item/LargeHeroBreakoutItem.svelte @@ -0,0 +1,268 @@ +<script lang="ts"> + import { + type Artwork as JetArtworkType, + type LargeHeroBreakout, + isFlowAction, + } from '@jet-app/app-store/api/models'; + import { isSome } from '@jet/environment/types/optional'; + + import LineClamp from '@amp/web-app-components/src/components/LineClamp/LineClamp.svelte'; + import mediaQueries from '~/utils/media-queries'; + import AppIcon from '~/components/AppIcon.svelte'; + import Artwork from '~/components/Artwork.svelte'; + import HoverWrapper from '~/components/HoverWrapper.svelte'; + import LinkWrapper from '~/components/LinkWrapper.svelte'; + import SFSymbol from '~/components/SFSymbol.svelte'; + import Video from '~/components/jet/Video.svelte'; + import type { NamedProfile } from '~/config/components/artwork'; + import { colorAsString, isRGBColor, isDark } from '~/utils/color'; + import { isRtl } from '~/utils/locale'; + import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html'; + + export let item: LargeHeroBreakout; + + let profile: NamedProfile; + let artwork: JetArtworkType | undefined; + let gradientColor: string; + + const { + collectionIcons = [], + editorialDisplayOptions, + rtlArtwork, + video, + details: { callToActionButtonAction: action }, + } = item; + const canUseRTLArtwork = isRtl() && rtlArtwork; + const shouldShowCollectionIcons = + collectionIcons?.length > 1 && !editorialDisplayOptions.suppressLockup; + + $: artwork = + (canUseRTLArtwork ? rtlArtwork : item.artwork) || video?.preview; + $: doesArtworkHaveDarkBackground = + artwork?.backgroundColor && + isRGBColor(artwork.backgroundColor) && + isDark(artwork.backgroundColor); + $: isBackgroundDark = item.isMediaDark ?? doesArtworkHaveDarkBackground; + + $: profile = + $mediaQueries === 'xsmall' + ? 'large-hero-portrait-iphone' + : canUseRTLArtwork + ? 'large-hero-breakout-rtl' + : 'large-hero-breakout'; + + $: gradientColor = artwork?.backgroundColor + ? colorAsString(artwork.backgroundColor) + : '#000'; +</script> + +<LinkWrapper {action}> + <HoverWrapper> + <div class="artwork-container"> + {#if video && $mediaQueries !== 'xsmall' && !canUseRTLArtwork} + <Video {video} {profile} autoplay loop useControls={false} /> + {:else if artwork} + <Artwork {artwork} {profile} /> + {/if} + </div> + + <div class="gradient" style="--color: {gradientColor};" /> + + <div + class="text-container" + class:on-dark={isBackgroundDark} + class:on-light={!isBackgroundDark} + > + {#if item.details?.badge} + <LineClamp clamp={1}> + <h4>{item.details.badge}</h4> + </LineClamp> + {/if} + + {#if item.details.title} + <LineClamp clamp={2}> + <h3>{@html sanitizeHtml(item.details.title)}</h3> + </LineClamp> + {/if} + + {#if item.details.description} + <LineClamp clamp={3}> + <p>{@html sanitizeHtml(item.details.description)}</p> + </LineClamp> + {/if} + + {#if isSome(action) && isFlowAction(action)} + <span class="link-container"> + {action.title} + <span aria-hidden="true"> + <SFSymbol name="chevron.forward" /> + </span> + </span> + {/if} + + {#if shouldShowCollectionIcons} + <ul class="collection-icons"> + {#each collectionIcons.slice(0, 6) as collectionIcon} + <li class="app-icon-container"> + <AppIcon icon={collectionIcon} /> + </li> + {/each} + </ul> + {/if} + </div> + </HoverWrapper> +</LinkWrapper> + +<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 *; + + .artwork-container { + width: 100%; + + @media (--range-small-up) { + aspect-ratio: 8 / 3; + } + } + + .artwork-container :global(.video-container) { + display: flex; + } + + .text-container { + position: absolute; + z-index: 2; + bottom: 0; + align-items: center; + width: 100%; + padding-inline: 20px; + padding-bottom: 20px; + text-wrap: pretty; + + @media (--range-small-up) { + width: 50%; + } + + @media (--range-large-up) { + width: 33%; + } + } + + .text-container.on-dark { + color: var(--systemPrimary-onDark); + + h4 { + color: var(--systemSecondary-onDark); + } + + :global(svg) { + fill: var(--systemPrimary-onDark); + } + } + + .text-container.on-light { + color: var(--systemPrimary-onLight); + + h4 { + color: var(--systemSecondary-onLight); + } + + :global(svg) { + fill: var(--systemPrimary-onLight); + } + } + + .link-container { + margin-top: 8px; + display: flex; + gap: 4px; + font: var(--body-emphasized); + + @media (--range-small-up) { + margin-top: 16px; + font: var(--title-2-emphasized); + } + } + + .link-container :global(svg) { + width: 8px; + height: 8px; + + @include rtl { + transform: rotate(180deg); + } + + @media (--range-small-up) { + width: 10px; + height: 10px; + } + } + + h3 { + text-wrap: balance; + font: var(--title-1-emphasized); + + @media (--range-small-up) { + font: var(--large-title-emphasized); + } + } + + h4 { + font: var(--subhead-emphasized); + + @media (--range-small-up) { + font: var(--callout-emphasized); + } + } + + p { + margin-top: 4px; + font: var(--body); + + @media (--range-small-up) { + margin-top: 8px; + font: var(--title-3); + } + } + + .collection-icons { + display: flex; + gap: 8px; + margin-top: 16px; + padding-top: 16px; + border-top: 2px solid var(--systemTertiary-onDark); + } + + .app-icon-container { + aspect-ratio: 1/1; + } + + .gradient { + --rotation: 35deg; + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + filter: saturate(1.5) brightness(0.9); + background: linear-gradient( + var(--rotation), + var(--color) 20%, + transparent 50% + ); + + // In non-XS viewports with an RTL text direction, we flip the legibility gradient to + // accomodate the right-justified text. + @include rtl { + @media (--range-small-up) { + --rotation: -35deg; + } + } + + // In XS viewports, this component is renderd in a 3/4 card layout, so we always want the + // gradient to be at 0deg rotation, as it goes from botttom to top. + @media (--range-xsmall-down) { + --rotation: 0deg; + } + } +</style> |
