refactor: extract ShowcaseCard component from models showcase

Move per-card markup out of ModelsShowcaseSection into a dedicated
ShowcaseCard component (mirroring GalleryCard). Switch the card to a
uniform h-80 height, animate the badge color on hover, and drop the
now-conflicting aspect-ratio classes that caused cards to overflow
their grid columns.
This commit is contained in:
Michael B
2026-05-22 13:51:14 -04:00
parent cc8ccf42c5
commit cd2e3219e3
2 changed files with 110 additions and 91 deletions

View File

@@ -1,13 +1,12 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import type { Locale, TranslationKey } from '../../i18n/translations'
import { externalLinks, getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
import ShowcaseCard from './ShowcaseCard.vue'
type ModelCard = {
export type ModelCard = {
titleKey: TranslationKey
slug: string
imageSrc: string
@@ -21,16 +20,13 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
const badgeBase =
'bg-white/20 text-white backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink'
const modelCards: ModelCard[] = [
{
titleKey: 'models.showcase.card.grokImagine',
slug: 'grok-image',
imageSrc: 'https://media.comfy.org/website/cloud/ai-models/grok-video.webm',
badgeIcon: '/icons/ai-models/grok.svg',
layoutClass: 'lg:col-span-6 lg:aspect-[16/7]'
layoutClass: 'lg:col-span-6'
},
{
titleKey: 'models.showcase.card.nanoBananaPro',
@@ -38,7 +34,7 @@ const modelCards: ModelCard[] = [
imageSrc:
'https://media.comfy.org/website/cloud/ai-models/nano-banana-pro.webp',
badgeIcon: '/icons/ai-models/gemini.svg',
layoutClass: 'lg:col-span-6 lg:aspect-[16/7]',
layoutClass: 'lg:col-span-6',
objectPosition: 'center 20%'
},
{
@@ -46,7 +42,7 @@ const modelCards: ModelCard[] = [
slug: 'ltxv-api',
imageSrc: 'https://media.comfy.org/website/gallery/desert.webp',
badgeText: 'ltx',
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
layoutClass: 'lg:col-span-4'
},
{
titleKey: 'models.showcase.card.qwenAdvancedEdit',
@@ -54,31 +50,24 @@ const modelCards: ModelCard[] = [
imageSrc:
'https://media.comfy.org/website/cloud/ai-models/qwen-image-edit.webp',
badgeIcon: '/icons/ai-models/qwen.svg',
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
layoutClass: 'lg:col-span-4'
},
{
titleKey: 'models.showcase.card.wan22TextToVideo',
slug: 'wan-api',
imageSrc: 'https://media.comfy.org/website/cloud/ai-models/wan-22.webm',
badgeIcon: '/icons/ai-models/wan.svg',
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
layoutClass: 'lg:col-span-4'
}
]
function getCardClass(layoutClass: string): string {
return cn(
layoutClass,
'group relative h-72 cursor-pointer overflow-hidden rounded-4xl bg-black/40 lg:h-auto'
)
}
function modelHref(slug: string): string {
return `${routes.models}/${slug}`
}
</script>
<template>
<section class="bg-primary-comfy-ink px-4 py-24 lg:px-20 lg:py-40">
<section class="px-4 py-24 lg:px-20 lg:py-40">
<div class="mx-auto flex w-full max-w-7xl flex-col items-center">
<p
class="text-primary-comfy-yellow text-center text-sm font-bold tracking-widest uppercase"
@@ -99,82 +88,16 @@ function modelHref(slug: string): string {
</p>
<div class="mt-24 w-full">
<div class="rounded-4xl border border-white/12 p-2 lg:p-1.5">
<div class="bg-transparency-white-t4 rounded-4xl p-2 lg:p-1.5">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-12">
<a
<ShowcaseCard
v-for="card in modelCards"
:key="card.titleKey"
:card="card"
:href="modelHref(card.slug)"
target="_blank"
rel="noopener noreferrer"
:class="getCardClass(card.layoutClass)"
>
<video
v-if="card.imageSrc.endsWith('.webm')"
:src="card.imageSrc"
:aria-label="t(card.titleKey, locale)"
:style="
card.objectPosition
? { objectPosition: card.objectPosition }
: undefined
"
class="size-full object-cover transition-transform duration-600 ease-in-out group-hover:scale-105"
autoplay
loop
muted
playsinline
/>
<img
v-else
:src="card.imageSrc"
:alt="t(card.titleKey, locale)"
:style="
card.objectPosition
? { objectPosition: card.objectPosition }
: undefined
"
class="size-full object-cover transition-transform duration-600 ease-in-out group-hover:scale-105"
loading="lazy"
decoding="async"
/>
<div
class="absolute inset-0 bg-linear-to-t from-black/50 via-black/5 to-black/35"
/>
<div
:class="
cn(
'absolute top-5 right-5 flex h-12 min-w-12 items-center justify-center px-3 lg:top-6 lg:right-6',
badgeBase,
'rounded-2xl'
)
"
>
<span
v-if="card.badgeIcon"
class="inline-block size-6 bg-current"
:style="{
maskImage: `url(${card.badgeIcon})`,
maskSize: 'contain',
maskRepeat: 'no-repeat',
maskPosition: 'center'
}"
/>
<span
v-else-if="card.badgeText"
class="text-xs font-bold tracking-wider lowercase"
>
{{ card.badgeText }}
</span>
</div>
<p
class="text-primary-warm-white absolute inset-x-6 bottom-6 text-2xl/tight font-light whitespace-pre-line drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)] lg:top-6 lg:right-auto lg:bottom-auto lg:text-3xl"
>
{{ t(card.titleKey, locale) }}
</p>
</a>
:locale="locale"
:class="card.layoutClass"
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'
import type { Locale } from '../../i18n/translations'
import type { ModelCard } from './ModelsShowcaseSection.vue'
import { t } from '../../i18n/translations'
const {
card,
href,
locale = 'en'
} = defineProps<{
card: ModelCard
href: string
locale?: Locale
}>()
const badgeBase =
'bg-white/20 text-white backdrop-blur-sm transition-colors duration-300 ease-in-out group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink'
</script>
<template>
<a
:href="href"
target="_blank"
rel="noopener noreferrer"
class="group relative h-80 cursor-pointer overflow-hidden rounded-4xl bg-black/40"
>
<video
v-if="card.imageSrc.endsWith('.webm')"
:src="card.imageSrc"
:aria-label="t(card.titleKey, locale)"
:style="
card.objectPosition
? { objectPosition: card.objectPosition }
: undefined
"
class="size-full object-cover transition-transform duration-600 ease-in-out group-hover:scale-105"
autoplay
loop
muted
playsinline
/>
<img
v-else
:src="card.imageSrc"
:alt="t(card.titleKey, locale)"
:style="
card.objectPosition
? { objectPosition: card.objectPosition }
: undefined
"
class="size-full object-cover transition-transform duration-600 ease-in-out group-hover:scale-105"
loading="lazy"
decoding="async"
/>
<div
class="absolute inset-0 bg-linear-to-t from-black/50 via-black/5 to-black/35"
/>
<div
:class="
cn(
'absolute top-5 right-5 flex h-12 min-w-12 items-center justify-center px-3 lg:top-6 lg:right-6',
badgeBase,
'rounded-2xl'
)
"
>
<span
v-if="card.badgeIcon"
class="inline-block size-6 bg-current"
:style="{
maskImage: `url(${card.badgeIcon})`,
maskSize: 'contain',
maskRepeat: 'no-repeat',
maskPosition: 'center'
}"
/>
<span
v-else-if="card.badgeText"
class="text-xs font-bold tracking-wider lowercase"
>
{{ card.badgeText }}
</span>
</div>
<p
class="text-primary-warm-white absolute inset-x-6 bottom-6 text-2xl/tight font-light whitespace-pre-line drop-shadow-[0_2px_8px_rgba(0,0,0,0.9)] lg:top-6 lg:right-auto lg:bottom-auto lg:text-3xl"
>
{{ t(card.titleKey, locale) }}
</p>
</a>
</template>