feat: AssetCard tweaks (#6085)

## Summary

1. fix `preview_url` logic
2. design tweaks for border radius, fallback gradient, and name line
clamping (2 lines)
3. handle image error states by showing gradient
4. misc. refactors

## Screenshots


<img width="1515" height="1087" alt="Screenshot 2025-10-15 at 8 13
41 PM"
src="https://github.com/user-attachments/assets/85642869-d8cb-4ee4-b23d-a381e33fe802"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6085-feat-AssetCard-tweaks-28e6d73d3650818da7a2f4148be48ff7)
by [Unito](https://www.unito.io)
This commit is contained in:
Arjan Singh
2025-10-16 14:46:50 -07:00
committed by GitHub
parent 9bfc9b740d
commit 03681a12bd
2 changed files with 99 additions and 23 deletions

View File

@@ -84,6 +84,51 @@ export const NonInteractive: Story = {
}
}
export const WithPreviewImage: Story = {
args: {
asset: createAssetData({
preview_url: '/assets/images/comfy-logo-single.svg'
}),
interactive: true
},
decorators: [
() => ({
template:
'<div class="p-8 bg-gray-50 dark-theme:bg-gray-900 max-w-96"><story /></div>'
})
],
parameters: {
docs: {
description: {
story: 'AssetCard with a preview image displayed.'
}
}
}
}
export const FallbackGradient: Story = {
args: {
asset: createAssetData({
preview_url: undefined
}),
interactive: true
},
decorators: [
() => ({
template:
'<div class="p-8 bg-gray-50 dark-theme:bg-gray-900 max-w-96"><story /></div>'
})
],
parameters: {
docs: {
description: {
story:
'AssetCard showing fallback gradient when no preview image is available.'
}
}
}
}
export const EdgeCases: Story = {
render: () => ({
components: { AssetCard },

View File

@@ -4,38 +4,29 @@
data-component-id="AssetCard"
:data-asset-id="asset.id"
v-bind="elementProps"
:class="
cn(
// Base layout and container styles (always applied)
'rounded-xl overflow-hidden transition-all duration-200',
interactive && 'group',
// Button-specific styles
interactive && [
'appearance-none bg-transparent p-0 m-0 font-inherit text-inherit outline-none cursor-pointer text-left',
'bg-gray-100 dark-theme:bg-charcoal-800',
'hover:bg-gray-200 dark-theme:hover:bg-charcoal-600',
'border-none',
'focus:outline-solid outline-blue-100 outline-4'
],
// Div-specific styles
!interactive && 'bg-gray-100 dark-theme:bg-charcoal-800'
)
"
:class="cardClasses"
@click="interactive && $emit('select', asset)"
@keydown.enter="interactive && $emit('select', asset)"
>
<div class="relative aspect-square w-full overflow-hidden">
<div class="relative aspect-square w-full overflow-hidden rounded-xl">
<img
v-if="shouldShowImage"
:src="asset.preview_url"
class="h-full w-full object-contain"
/>
<div
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-600"
v-else
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-gray-400 via-gray-800 to-charcoal-400"
></div>
<AssetBadgeGroup :badges="asset.badges" />
</div>
<div :class="cn('p-4 h-32 flex flex-col justify-between')">
<div>
<h3
:id="titleId"
:class="
cn(
'mb-2 m-0 text-base font-semibold overflow-hidden text-ellipsis whitespace-nowrap',
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
'text-slate-800',
'dark-theme:text-white'
)
@@ -44,6 +35,7 @@
{{ asset.name }}
</h3>
<p
:id="descId"
:class="
cn(
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
@@ -83,7 +75,8 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useImage } from '@vueuse/core'
import { computed, useId } from 'vue'
import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
@@ -94,13 +87,51 @@ const props = defineProps<{
interactive?: boolean
}>()
const titleId = useId()
const descId = useId()
const { error } = useImage({
src: props.asset.preview_url ?? '',
alt: props.asset.name
})
const shouldShowImage = computed(() => props.asset.preview_url && !error.value)
const cardClasses = computed(() => {
const base = [
'rounded-xl',
'overflow-hidden',
'transition-all',
'duration-200'
]
if (!props.interactive) {
return cn(...base, 'bg-gray-100 dark-theme:bg-charcoal-800')
}
return cn(
...base,
'group',
'appearance-none bg-transparent p-0 m-0',
'font-inherit text-inherit outline-none cursor-pointer text-left',
'bg-gray-100 dark-theme:bg-charcoal-800',
'hover:bg-gray-200 dark-theme:hover:bg-charcoal-600',
'border-none',
'focus:outline-solid outline-blue-100 outline-4'
)
})
const elementProps = computed(() =>
props.interactive
? {
type: 'button',
'aria-label': `Select asset ${props.asset.name}`
'aria-labelledby': titleId,
'aria-describedby': descId
}
: {
'aria-labelledby': titleId,
'aria-describedby': descId
}
: {}
)
defineEmits<{