diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /src/components/jet/item/MediumStoryCard/EditorialStoryCardItem.svelte | |
init commit
Diffstat (limited to 'src/components/jet/item/MediumStoryCard/EditorialStoryCardItem.svelte')
| -rw-r--r-- | src/components/jet/item/MediumStoryCard/EditorialStoryCardItem.svelte | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/src/components/jet/item/MediumStoryCard/EditorialStoryCardItem.svelte b/src/components/jet/item/MediumStoryCard/EditorialStoryCardItem.svelte new file mode 100644 index 0000000..7b7807c --- /dev/null +++ b/src/components/jet/item/MediumStoryCard/EditorialStoryCardItem.svelte @@ -0,0 +1,304 @@ +<script lang="ts"> + import { + isFlowAction, + type EditorialStoryCard, + type FlowAction, + } from '@jet-app/app-store/api/models'; + import type { Opt } from '@jet/environment'; + + 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 LinkWrapper from '~/components/LinkWrapper.svelte'; + import { getI18n } from '~/stores/i18n'; + import HoverWrapper from '~/components/HoverWrapper.svelte'; + + export let item: EditorialStoryCard; + + let { + clickAction, + collectionIcons, + title, + lockup: { title: lockupTitle, subtitle, heading: lockupHeading } = {}, + } = item; + const i18n = getI18n(); + const hasMultipleCollectionIcons = (collectionIcons?.length ?? 0) > 1; + const destination: Opt<FlowAction> = + clickAction && isFlowAction(clickAction) ? clickAction : undefined; +</script> + +<LinkWrapper action={destination}> + <article> + {#if item.artwork} + <div class="artwork-container"> + <HoverWrapper element="div"> + <Artwork + artwork={item.artwork} + profile="editorial-story-card" + /> + </HoverWrapper> + </div> + {/if} + <div class="details-container"> + <div + class="title-container" + class:on-dark={item.isMediaDark} + class:on-light={!item.isMediaDark} + > + {#if item.badge} + <h4>{item.badge.title}</h4> + {/if} + + {#if item.title} + <h3>{@html sanitizeHtml(item.title)}</h3> + {/if} + + {#if item.description} + <p>{@html sanitizeHtml(item.description)}</p> + {/if} + </div> + + {#if collectionIcons && !item.editorialDisplayOptions.suppressLockup} + <div class="lockup-container"> + <ul class:with-multiple-icons={hasMultipleCollectionIcons}> + {#each collectionIcons as collectionIcon} + <li class="app-icon-container"> + <AppIcon + icon={collectionIcon} + fixedWidth={false} + profile={hasMultipleCollectionIcons + ? 'app-icon-medium' + : 'app-icon'} + /> + </li> + {/each} + </ul> + + {#if !hasMultipleCollectionIcons} + <div class="metadata-container"> + {#if lockupHeading} + <span class="lockup-eyebrow"> + {lockupHeading} + </span> + {/if} + + <!-- + Some cards with the lockup UI don't have a `lockup` property, + so we use the title of the item as a fallback. + --> + {#if lockupTitle || title} + <LineClamp clamp={1}> + <h4 class="lockup-title"> + {lockupTitle || title} + </h4> + </LineClamp> + {/if} + + {#if subtitle} + <LineClamp clamp={1}> + <p class="lockup-subtitle">{subtitle}</p> + </LineClamp> + {/if} + </div> + + {#if destination} + <div class="button-container"> + <span class="get-button transparent"> + {$i18n.t('ASE.Web.AppStore.View')} + </span> + </div> + {/if} + {/if} + </div> + {/if} + </div> + <div + class="blur-overlay" + style:--brightness={item.isMediaDark ? 0.75 : 1.25} + /> + </article> +</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 *; + + article { + position: relative; + overflow: hidden; + border-radius: var(--global-border-radius-large); + box-shadow: var(--shadow-medium); + aspect-ratio: 3/4; + container-type: inline-size; + container-name: card; + } + + .artwork-container { + position: absolute; + width: 100%; + height: 100%; + } + + .details-container { + display: flex; + flex-direction: column; + justify-content: end; + height: 100%; + border-radius: var(--global-border-radius-large); + overflow: hidden; + z-index: 1; + } + + .title-container { + padding: 20px; + z-index: 2; + } + + .title-container h3 { + margin-bottom: 2px; + font: var(--title-1-emphasized); + text-wrap: pretty; + } + + .title-container h4 { + font: var(--callout-emphasized); + } + + .on-dark { + color: var(--systemPrimary-onDark); + } + + .on-light { + color: var(--systemPrimary-onLight); + } + + .title-container.on-dark h4 { + color: var(--systemSecondary-onDark); + mix-blend-mode: plus-lighter; + } + + .title-container.on-light h4 { + color: var(--systemSecondary-onLight); + } + + .title-container.on-dark p { + font: var(--body); + color: var(--systemSecondary-onDark); + } + + .title-container.on-light p { + font: var(--body); + color: var(--systemSecondary-onLight); + } + + .lockup-container { + display: flex; + align-items: center; + min-height: 80px; + padding: 10px 20px; + color: var(--systemPrimary-onDark); + background-image: linear-gradient(rgba(0, 0, 0, 0.2) 0 0); + z-index: 2; + } + + .metadata-container { + flex-grow: 1; + margin-inline-end: 16px; + } + + .lockup-title { + font: var(--title-3-emphasized); + } + + .lockup-eyebrow { + color: var(--systemSecondary-onDark); + font: var(--subhead-emphasized); + text-transform: uppercase; + mix-blend-mode: plus-lighter; + } + + .lockup-subtitle { + color: var(--systemSecondary-onDark); + font: var(--callout); + mix-blend-mode: plus-lighter; + } + + .app-icon-container { + flex-shrink: 0; + width: 48px; + margin-inline-end: 16px; + } + + article:hover .blur-overlay { + height: 52%; + backdrop-filter: blur(70px) saturate(1.5) + brightness(calc(var(--brightness) * 0.9)); + } + + .blur-overlay { + position: absolute; + z-index: 1; + top: unset; + bottom: 0; + width: 100%; + height: 50%; + border-radius: var(--global-border-radius-large); + mask-image: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 5%, + rgba(0, 0, 0, 1) 50% + ); + backdrop-filter: blur(50px) saturate(1.5) + brightness((var(--brightness))); + transition-property: height, backdrop-filter; + transition-duration: 210ms; + transition-timing-function: ease-out; + } + + ul.with-multiple-icons { + width: 100%; + display: grid; + gap: 12px; + + .app-icon-container { + width: 100%; + margin-inline-end: unset; + } + } + + // In the following container queries, we are specifying column counts and hiding icons past + // that number to ensure a reasonable number of icons are shown for different size cards. + @container card (max-width: 300px) { + ul.with-multiple-icons { + // Think of "4" as the number of columns to show + grid-template-columns: repeat(4, 1fr); + } + + // And "5" as the number of columns to hide past + .app-icon-container:nth-child(n + 5) { + display: none; + } + } + + @container card (min-width: 300px) and (max-width: 400px) { + ul.with-multiple-icons { + grid-template-columns: repeat(5, 1fr); + } + + .app-icon-container:nth-child(n + 6) { + display: none; + } + } + + @container card (min-width: 400px) { + ul.with-multiple-icons { + grid-template-columns: repeat(6, 1fr); + } + + .app-icon-container:nth-child(n + 7) { + display: none; + } + } +</style> |
