this repo has no description
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 536 lines 16 kB view raw
1<!-- 2@component 3Component for rendering an item in a "Hero Carousel" without coupling to any specific data model 4--> 5<script lang="ts"> 6 import type { Opt } from '@jet/environment/types/optional'; 7 import type { 8 Action, 9 Artwork as ArtworkModel, 10 Color, 11 Video as VideoModel, 12 } from '@jet-app/app-store/api/models'; 13 14 import mediaQueries from '~/utils/media-queries'; 15 import { prefersReducedMotion } from '@amp/web-app-components/src/stores/prefers-reduced-motion'; 16 import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html'; 17 18 import AppIcon from '~/components/AppIcon.svelte'; 19 import Artwork from '~/components/Artwork.svelte'; 20 import LinkWrapper from '~/components/LinkWrapper.svelte'; 21 import Video from '~/components/jet/Video.svelte'; 22 import type { NamedProfile } from '~/config/components/artwork'; 23 import { 24 colorAsString, 25 getBackgroundGradientCSSVarsFromArtworks, 26 getLuminanceForRGB, 27 } from '~/utils/color'; 28 import { isRtl } from '~/utils/locale'; 29 30 /** 31 * The main text for the carousel item 32 */ 33 export let title: Opt<string> = undefined; 34 35 /** 36 * Additional text above the title. 37 * Note: If a slot is defined with the name `eyebrow`, the slot takes precedence. 38 */ 39 export let eyebrow: Opt<string> = undefined; 40 41 /** 42 * Additional text below the title 43 */ 44 export let subtitle: Opt<string> = undefined; 45 46 /** 47 * Primary accent color for the carousel item 48 */ 49 export let backgroundColor: Opt<Color> = undefined; 50 51 /** 52 * Static artwork to display in the carousel item 53 */ 54 export let artwork: Opt<ArtworkModel> = undefined; 55 56 /** 57 * Video to display in the carousel item 58 * 59 * Takes precedence over `artwork` 60 */ 61 export let video: Opt<VideoModel> = undefined; 62 63 /** 64 * Action to perform when clicking on the carousel item 65 */ 66 export let action: Opt<Action> = undefined; 67 68 /** 69 * Whether the artwork should be aligned to the end (e.g. the right edge in LTR) of the container 70 */ 71 export let pinArtworkToHorizontalEnd: boolean = false; 72 73 /** 74 * Whether the artwork should be pinned to the vertical middle of the container (it's pinned to the top by default) 75 */ 76 export let pinArtworkToVerticalMiddle: boolean = false; 77 78 /** 79 * Whether the text (e.g. title, description, etc) should be pinned to the top of the container 80 */ 81 export let pinTextToVerticalStart: boolean = false; 82 83 /** 84 * Allows for the absolute overriding of the profile used for the Hero artwork 85 */ 86 export let profileOverride: Opt<NamedProfile> = null; 87 88 export let isMediaDark: boolean = true; 89 90 export let collectionIcons: ArtworkModel[] | undefined = undefined; 91 92 let isPortraitLayout: boolean; 93 let profile: NamedProfile; 94 let collectionIconsBackgroundGradientCssVars: string | undefined = 95 undefined; 96 97 $: isPortraitLayout = $mediaQueries === 'xsmall'; 98 99 $: { 100 if (profileOverride) { 101 profile = profileOverride; 102 } else if (isPortraitLayout) { 103 profile = 'large-hero-portrait'; 104 } else if (pinArtworkToHorizontalEnd && isRtl()) { 105 profile = 'large-hero-east'; 106 } else if (pinArtworkToHorizontalEnd) { 107 profile = 'large-hero-west'; 108 } else { 109 profile = 'large-hero'; 110 } 111 } 112 113 const color: string = backgroundColor 114 ? colorAsString(backgroundColor) 115 : '#000'; 116 117 if (collectionIcons && collectionIcons.length > 1) { 118 // If there are multiple app icons, we build a string of CSS variables from the icons 119 // background colors to fill as many of the lockups quadrants as possible. 120 collectionIconsBackgroundGradientCssVars = 121 getBackgroundGradientCSSVarsFromArtworks(collectionIcons, { 122 // sorts from darkest to lightest 123 sortFn: (a, b) => getLuminanceForRGB(a) - getLuminanceForRGB(b), 124 shouldRemoveGreys: true, 125 }); 126 } 127</script> 128 129<LinkWrapper {action} includeExternalLinkArrowIcon={false}> 130 <article 131 data-test-id="hero" 132 class:with-dark-media={isMediaDark} 133 class:with-collection-icons={!artwork && !video && collectionIcons} 134 class:text-pinned-to-vertical-start={pinTextToVerticalStart} 135 > 136 {#if video || artwork} 137 <div 138 class={`image-container ${profile}`} 139 class:pinned-to-horizontal-end={pinArtworkToHorizontalEnd} 140 class:pinned-to-vertical-middle={pinArtworkToVerticalMiddle} 141 style:--color={color} 142 > 143 {#if video && !$prefersReducedMotion} 144 <Video 145 loop 146 autoplay 147 useControls={false} 148 {video} 149 {profile} 150 /> 151 {:else if artwork} 152 <Artwork 153 {artwork} 154 {profile} 155 noShelfChevronAnchor={true} 156 useCropCodeFromArtwork={false} 157 withoutBorder={true} 158 /> 159 {/if} 160 </div> 161 {:else if collectionIcons} 162 <ul class="app-icons"> 163 {#each collectionIcons?.slice(0, 5) as collectionIcon} 164 <li class="app-icon-container"> 165 <AppIcon 166 icon={collectionIcon} 167 profile="app-icon-large" 168 fixedWidth={false} 169 /> 170 </li> 171 {/each} 172 </ul> 173 174 <div 175 class="collection-icons-background-gradient" 176 style={collectionIconsBackgroundGradientCssVars} 177 /> 178 {/if} 179 180 <div class="gradient" style="--color: {color};" /> 181 182 <slot name="badge" {isPortraitLayout} /> 183 184 <div class="metadata-container"> 185 {#if $$slots.eyebrow} 186 <h3><slot name="eyebrow" /></h3> 187 {:else if eyebrow} 188 <h3>{eyebrow}</h3> 189 {/if} 190 191 {#if title} 192 <h2>{@html sanitizeHtml(title)}</h2> 193 {/if} 194 195 {#if subtitle} 196 <p class="subtitle">{@html sanitizeHtml(subtitle)}</p> 197 {/if} 198 199 <slot name="details" {isPortraitLayout} /> 200 </div> 201 </article> 202</LinkWrapper> 203 204<style lang="scss"> 205 @use '@amp/web-shared-styles/app/core/globalvars' as *; 206 207 article { 208 --hero-primary-color: var(--systemPrimary-onLight); 209 --hero-secondary-color: var(--systemSecondary-onLight); 210 --hero-text-blend-mode: normal; 211 --hero-divider-color: var(--systemQuaternary-onLight); 212 position: relative; 213 display: flex; 214 overflow: hidden; 215 align-items: end; 216 aspect-ratio: 3 / 4; 217 container-name: hero-container; 218 container-type: size; 219 220 @media (--range-small-up) { 221 aspect-ratio: 16 / 9; 222 width: 100%; 223 height: auto; 224 min-height: 360px; 225 max-height: min(60vh, 770px); 226 border-radius: var(--global-border-radius-large); 227 border: 1px solid var(--systemQuaternary); 228 } 229 } 230 231 article.with-dark-media, 232 article.with-collection-icons { 233 --hero-primary-color: var(--systemPrimary-onDark); 234 --hero-secondary-color: var(--systemSecondary-onDark); 235 --hero-divider-color: var(--systemQuaternary-onDark); 236 --hero-text-blend-mode: plus-lighter; 237 } 238 239 .image-container { 240 position: absolute; 241 z-index: -1; 242 width: 100%; 243 height: 100%; 244 background-color: var(--color); 245 } 246 247 .image-container.pinned-to-vertical-middle { 248 display: flex; 249 align-items: center; 250 } 251 252 .image-container.pinned-to-vertical-middle :global(.video-container), 253 .image-container.pinned-to-vertical-middle :global(.artwork-component) { 254 width: 100%; 255 height: auto; 256 } 257 258 .image-container.pinned-to-horizontal-end :global(.artwork-component) { 259 height: 100%; 260 display: flex; 261 } 262 263 .image-container.pinned-to-horizontal-end :global(.artwork-component img) { 264 height: 100%; 265 width: auto; 266 position: absolute; 267 inset-inline-end: 0; 268 269 @container hero-container (aspect-ratio >= 279/100) { 270 width: 100%; 271 height: auto; 272 } 273 } 274 275 .image-container.pinned-to-horizontal-end.large-hero-story-card-rtl 276 :global(.artwork-component img) { 277 inset-inline-start: 0; 278 } 279 280 // This is terrible but essentially the `large-hero-story-card` profile has an aspect ratio of 281 // 2.25:1, so whenever the image container gets expanded past that aspect ratio, we make the 282 // artwork full-width rather than full-height. This should eventually be fixed when Editorial 283 // can prescribe us only 16x9 (1.77:1) hero images. 284 .image-container.pinned-to-horizontal-end.large-hero-story-card, 285 .image-container.pinned-to-horizontal-end.large-hero-story-card-rtl { 286 @container hero-container (aspect-ratio >= 225/100) { 287 :global(.artwork-component img) { 288 width: 100%; 289 height: auto; 290 } 291 } 292 } 293 294 .metadata-container { 295 position: absolute; 296 width: 40%; 297 padding-bottom: 40px; 298 padding-inline-start: 40px; 299 text-wrap: pretty; 300 color: var(--hero-primary-color); 301 302 @media (--range-small-only) { 303 width: 50%; 304 padding: 0 20px 20px; 305 } 306 307 @media (--range-xsmall-down) { 308 width: 100%; 309 padding: 0 20px 20px; 310 text-align: center; 311 } 312 } 313 314 .text-pinned-to-vertical-start .metadata-container { 315 @media (--range-small-only) { 316 top: 20px; 317 } 318 319 @media (--range-medium-up) { 320 top: 40px; 321 } 322 } 323 324 h2 { 325 position: relative; 326 z-index: 1; 327 text-wrap: balance; 328 font: var(--header-emphasized); 329 330 @media (--range-xsmall-down) { 331 font: var(--title-1-emphasized); 332 } 333 } 334 335 @container hero-container (height < 420px) { 336 h2 { 337 font: var(--large-title-emphasized); 338 } 339 } 340 341 h3 { 342 margin-bottom: 8px; 343 position: relative; 344 z-index: 1; 345 color: var(--hero-secondary-color); 346 font: var(--callout-emphasized-tall); 347 mix-blend-mode: var(--hero-text-blend-mode); 348 349 @media (--range-xsmall-down) { 350 margin-bottom: 4px; 351 } 352 } 353 354 p { 355 mix-blend-mode: var(--hero-text-blend-mode); 356 } 357 358 .subtitle { 359 margin-top: 8px; 360 position: relative; 361 z-index: 1; 362 font: var(--body-tall); 363 color: var(--hero-secondary-color); 364 } 365 366 .gradient { 367 --rotation: 55deg; 368 369 &:dir(rtl) { 370 --rotation: -55deg; 371 mask-image: radial-gradient( 372 ellipse 127% 130% at 95% 100%, 373 rgb(0, 0, 0) 18%, 374 rgb(0, 0, 0.33) 24%, 375 rgba(0, 0, 0, 0.66) 32%, 376 transparent 40% 377 ), 378 linear-gradient( 379 -129deg, 380 rgb(0, 0, 0) 0%, 381 rgba(255, 255, 255, 0) 55% 382 ); 383 } 384 position: absolute; 385 z-index: -1; 386 width: 100%; 387 height: 100%; 388 // stylelint-disable color-function-notation 389 background: linear-gradient( 390 var(--rotation), 391 rgb(from var(--color) r g b / 0.25) 0%, 392 transparent 50% 393 ); 394 // stylelint-enable color-function-notation 395 filter: saturate(1.5) brightness(0.9); 396 backdrop-filter: blur(40px); 397 mask-image: radial-gradient( 398 ellipse 127% 130% at 5% 100%, 399 rgb(0, 0, 0) 18%, 400 rgb(0, 0, 0.33) 24%, 401 rgba(0, 0, 0, 0.66) 32%, 402 transparent 40% 403 ), 404 linear-gradient(51deg, rgb(0, 0, 0) 0%, rgba(255, 255, 255, 0) 55%); 405 406 @media (--range-xsmall-down) { 407 --rotation: 0deg; 408 mask-image: linear-gradient( 409 var(--rotation), 410 rgb(0, 0, 0) 28%, 411 rgba(0, 0, 0, 0) 56% 412 ); 413 } 414 } 415 416 // When the text is pinned to the top of the lockup, we use a different gradient for legibility 417 article.text-pinned-to-vertical-start .gradient { 418 --rotation: -170deg; 419 mask-image: radial-gradient( 420 ellipse 118% 121% at 100% 0%, 421 rgb(0, 0, 0) 18%, 422 rgb(0, 0, 0.33) 22%, 423 rgba(0, 0, 0, 0.66) 33%, 424 transparent 43% 425 ); 426 } 427 428 .app-icons { 429 display: grid; 430 align-self: center; 431 width: 90%; 432 grid-template-rows: auto auto; 433 grid-auto-flow: column; 434 gap: 24px; 435 margin-inline-start: -4%; 436 position: absolute; 437 inset-inline-end: 24px; 438 439 @media (--range-small-up) { 440 width: 44%; 441 } 442 } 443 444 .app-icons li:nth-child(even) { 445 inset-inline-start: 44%; 446 } 447 448 .app-icon-container { 449 position: relative; 450 flex-shrink: 0; 451 max-width: 200px; 452 } 453 454 @property --top-left-stop { 455 syntax: '<percentage>'; 456 inherits: false; 457 initial-value: 20%; 458 } 459 460 @property --bottom-left-stop { 461 syntax: '<percentage>'; 462 inherits: false; 463 initial-value: 40%; 464 } 465 466 @property --top-right-stop { 467 syntax: '<percentage>'; 468 inherits: false; 469 initial-value: 55%; 470 } 471 472 @property --bottom-right-stop { 473 syntax: '<percentage>'; 474 inherits: false; 475 initial-value: 50%; 476 } 477 478 .collection-icons-background-gradient { 479 width: 100%; 480 height: 100%; 481 position: absolute; 482 background: radial-gradient( 483 circle at 3% -50%, 484 var(--top-left, #000) var(--top-left-stop), 485 transparent 70% 486 ), 487 radial-gradient( 488 circle at -50% 120%, 489 var(--bottom-left, #000) var(--bottom-left-stop), 490 transparent 80% 491 ), 492 radial-gradient( 493 circle at 66% -175%, 494 var(--top-right, #000) var(--top-right-stop), 495 transparent 80% 496 ), 497 radial-gradient( 498 circle at 62% 100%, 499 var(--bottom-right, #000) var(--bottom-right-stop), 500 transparent 100% 501 ); 502 animation: collection-icons-background-gradient-shift 16s infinite 503 alternate-reverse; 504 animation-play-state: paused; 505 506 @media (--range-small-up) { 507 animation-play-state: running; 508 } 509 } 510 511 @keyframes collection-icons-background-gradient-shift { 512 0% { 513 --top-left-stop: 20%; 514 --bottom-left-stop: 40%; 515 --top-right-stop: 55%; 516 --bottom-right-stop: 50%; 517 background-size: 100% 100%; 518 } 519 520 50% { 521 --top-left-stop: 25%; 522 --bottom-left-stop: 15%; 523 --top-right-stop: 70%; 524 --bottom-right-stop: 30%; 525 background-size: 130% 130%; 526 } 527 528 100% { 529 --top-left-stop: 15%; 530 --bottom-left-stop: 20%; 531 --top-right-stop: 55%; 532 --bottom-right-stop: 20%; 533 background-size: 110% 110%; 534 } 535 } 536</style>