summaryrefslogtreecommitdiff
path: root/src/components/jet/item/BrickItem.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/jet/item/BrickItem.svelte')
-rw-r--r--src/components/jet/item/BrickItem.svelte300
1 files changed, 300 insertions, 0 deletions
diff --git a/src/components/jet/item/BrickItem.svelte b/src/components/jet/item/BrickItem.svelte
new file mode 100644
index 0000000..a9e6319
--- /dev/null
+++ b/src/components/jet/item/BrickItem.svelte
@@ -0,0 +1,300 @@
+<script lang="ts">
+ import type { Brick } from '@jet-app/app-store/api/models';
+
+ import LineClamp from '@amp/web-app-components/src/components/LineClamp/LineClamp.svelte';
+ import { sanitizeHtml } from '@amp/web-app-components/src/utils/sanitize-html';
+ import AppIcon from '~/components/AppIcon.svelte';
+ import Artwork from '~/components/Artwork.svelte';
+ import GradientOverlay from '~/components/GradientOverlay.svelte';
+ import HoverWrapper from '~/components/HoverWrapper.svelte';
+ import LinkWrapper from '~/components/LinkWrapper.svelte';
+ import {
+ colorAsString,
+ getBackgroundGradientCSSVarsFromArtworks,
+ getLuminanceForRGB,
+ } from '~/utils/color';
+ import { isRtl } from '~/utils/locale';
+
+ export let item: Brick;
+ export let shouldOverlayDescription: boolean = false;
+
+ const rtlArtwork = item.artworks?.[1] || item.rtlArtwork;
+ const artwork = isRtl() && rtlArtwork ? rtlArtwork : item.artworks?.[0];
+ const { collectionIcons } = item;
+
+ const gradientColor: string = artwork?.backgroundColor
+ ? colorAsString(artwork.backgroundColor)
+ : 'rgb(0 0 0 / 62%)';
+
+ let backgroundGradientCssVars: string | undefined = undefined;
+
+ if (collectionIcons && collectionIcons.length > 1) {
+ // If there are multiple app icons, we build a string of CSS variables from the icons
+ // background colors to fill as many of the lockups quadrants as possible.
+ backgroundGradientCssVars = getBackgroundGradientCSSVarsFromArtworks(
+ collectionIcons,
+ {
+ // sorts from darkest to lightest
+ sortFn: (a, b) => getLuminanceForRGB(a) - getLuminanceForRGB(b),
+ shouldRemoveGreys: true,
+ },
+ );
+ }
+</script>
+
+<LinkWrapper
+ action={item.clickAction}
+ label={item.accessibilityLabel || item.clickAction?.title}
+>
+ <div class="container">
+ <HoverWrapper>
+ {#if artwork}
+ <Artwork
+ {artwork}
+ profile={shouldOverlayDescription ? 'small-brick' : 'brick'}
+ />
+ {:else if backgroundGradientCssVars}
+ <div
+ class="background-gradient"
+ style={backgroundGradientCssVars}
+ />
+ {/if}
+
+ {#if item.title}
+ <GradientOverlay --color={gradientColor} />
+ {/if}
+
+ <div class="text-container">
+ <div class="metadata-container">
+ {#if item.caption}
+ <LineClamp clamp={1}>
+ <h4>{item.caption}</h4>
+ </LineClamp>
+ {/if}
+
+ {#if item.title}
+ <LineClamp clamp={3}>
+ <h3 class="title">
+ {@html sanitizeHtml(item.title)}
+ </h3>
+ </LineClamp>
+ {/if}
+
+ {#if item.subtitle}
+ <LineClamp clamp={2}>
+ <p>{item.subtitle}</p>
+ </LineClamp>
+ {/if}
+ </div>
+
+ {#if !artwork && collectionIcons}
+ <ul class="app-icons">
+ {#each collectionIcons?.slice(0, 8) as collectionIcon}
+ <li class="app-icon-container">
+ <AppIcon
+ icon={collectionIcon}
+ profile="brick-app-icon"
+ fixedWidth={false}
+ />
+ </li>
+ {/each}
+ </ul>
+ {/if}
+ </div>
+ </HoverWrapper>
+
+ {#if item.shortEditorialDescription}
+ <h3
+ class="editorial-description"
+ class:overlaid={shouldOverlayDescription}
+ >
+ {item.shortEditorialDescription}
+ </h3>
+ {/if}
+ </div>
+</LinkWrapper>
+
+<style>
+ .container {
+ position: relative;
+ container-type: inline-size;
+ container-name: container;
+ }
+
+ .metadata-container {
+ width: 100%;
+ align-self: end;
+ }
+
+ .text-container {
+ position: absolute;
+ z-index: 2;
+ bottom: 0;
+ display: flex;
+ align-items: flex-end;
+ width: 100%;
+ height: 100%;
+ padding: 20px;
+ color: var(--systemPrimary-onDark);
+ }
+
+ .app-icon-container {
+ position: relative;
+ flex-shrink: 0;
+ width: 60px;
+ margin-inline-end: 5%;
+ }
+
+ .title {
+ font: var(--title-1-emphasized);
+ text-wrap: pretty;
+ }
+
+ h4 {
+ margin-bottom: 3px;
+ font: var(--callout-emphasized);
+ }
+
+ p {
+ margin-top: 6px;
+ font: var(--body-emphasized);
+ }
+
+ .editorial-description {
+ margin-top: 8px;
+ font: var(--title-3);
+ }
+
+ .editorial-description.overlaid {
+ position: absolute;
+ z-index: 1;
+ bottom: 9px;
+ padding: 0 20px;
+ color: white;
+ font: var(--title-2-emphasized);
+ }
+
+ @property --top-left-stop {
+ syntax: '<percentage>';
+ inherits: false;
+ initial-value: 20%;
+ }
+
+ @property --bottom-left-stop {
+ syntax: '<percentage>';
+ inherits: false;
+ initial-value: 40%;
+ }
+
+ @property --top-right-stop {
+ syntax: '<percentage>';
+ inherits: false;
+ initial-value: 55%;
+ }
+
+ @property --bottom-right-stop {
+ syntax: '<percentage>';
+ inherits: false;
+ initial-value: 50%;
+ }
+
+ .container .background-gradient {
+ width: 100%;
+ aspect-ratio: 16 / 9;
+ background: radial-gradient(
+ circle at 3% -50%,
+ var(--top-left, #000) var(--top-left-stop),
+ transparent 70%
+ ),
+ radial-gradient(
+ circle at -50% 120%,
+ var(--bottom-left, #000) var(--bottom-left-stop),
+ transparent 80%
+ ),
+ radial-gradient(
+ circle at 66% -175%,
+ var(--top-right, #000) var(--top-right-stop),
+ transparent 80%
+ ),
+ radial-gradient(
+ circle at 62% 100%,
+ var(--bottom-right, #000) var(--bottom-right-stop),
+ transparent 100%
+ );
+ animation: gradient-hover 8s infinite alternate-reverse;
+ animation-play-state: paused;
+ }
+
+ @keyframes gradient-hover {
+ 0% {
+ --top-left-stop: 20%;
+ --bottom-left-stop: 40%;
+ --top-right-stop: 55%;
+ --bottom-right-stop: 50%;
+ background-size: 100% 100%;
+ }
+
+ 50% {
+ --top-left-stop: 25%;
+ --bottom-left-stop: 15%;
+ --top-right-stop: 70%;
+ --bottom-right-stop: 30%;
+ background-size: 130% 130%;
+ }
+
+ 100% {
+ --top-left-stop: 15%;
+ --bottom-left-stop: 20%;
+ --top-right-stop: 55%;
+ --bottom-right-stop: 20%;
+ background-size: 110% 110%;
+ }
+ }
+
+ .container:hover .background-gradient {
+ animation-play-state: running;
+ }
+
+ .app-icons {
+ display: grid;
+ align-self: center;
+ flex-direction: row;
+ width: 44%;
+ grid-template-rows: auto auto;
+ grid-auto-flow: column;
+ gap: 8px;
+ }
+
+ .app-icons li:nth-child(even) {
+ inset-inline-start: 40px;
+ }
+
+ @container container (max-width: 298px) {
+ .title {
+ font: var(--title-2-emphasized);
+ }
+
+ .text-container {
+ padding: 16px;
+ }
+
+ .editorial-description.overlaid {
+ bottom: 16px;
+ padding-inline: 16px;
+ }
+
+ .app-icons {
+ width: 36%;
+ }
+
+ .app-icon-container {
+ width: 50px;
+ }
+ }
+
+ @container container (min-width: 440px) {
+ .app-icon-container {
+ width: 83px;
+ }
+ }
+</style>