mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 23:20:07 +00:00
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:
@@ -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 = {
|
export const EdgeCases: Story = {
|
||||||
render: () => ({
|
render: () => ({
|
||||||
components: { AssetCard },
|
components: { AssetCard },
|
||||||
|
|||||||
@@ -4,38 +4,29 @@
|
|||||||
data-component-id="AssetCard"
|
data-component-id="AssetCard"
|
||||||
:data-asset-id="asset.id"
|
:data-asset-id="asset.id"
|
||||||
v-bind="elementProps"
|
v-bind="elementProps"
|
||||||
:class="
|
:class="cardClasses"
|
||||||
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'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
@click="interactive && $emit('select', asset)"
|
@click="interactive && $emit('select', asset)"
|
||||||
@keydown.enter="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
|
<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>
|
></div>
|
||||||
<AssetBadgeGroup :badges="asset.badges" />
|
<AssetBadgeGroup :badges="asset.badges" />
|
||||||
</div>
|
</div>
|
||||||
<div :class="cn('p-4 h-32 flex flex-col justify-between')">
|
<div :class="cn('p-4 h-32 flex flex-col justify-between')">
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3
|
||||||
|
:id="titleId"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
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',
|
'text-slate-800',
|
||||||
'dark-theme:text-white'
|
'dark-theme:text-white'
|
||||||
)
|
)
|
||||||
@@ -44,6 +35,7 @@
|
|||||||
{{ asset.name }}
|
{{ asset.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<p
|
<p
|
||||||
|
:id="descId"
|
||||||
:class="
|
:class="
|
||||||
cn(
|
cn(
|
||||||
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
|
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
|
||||||
@@ -83,7 +75,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
|
||||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||||
@@ -94,13 +87,51 @@ const props = defineProps<{
|
|||||||
interactive?: boolean
|
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(() =>
|
const elementProps = computed(() =>
|
||||||
props.interactive
|
props.interactive
|
||||||
? {
|
? {
|
||||||
type: 'button',
|
type: 'button',
|
||||||
'aria-label': `Select asset ${props.asset.name}`
|
'aria-labelledby': titleId,
|
||||||
|
'aria-describedby': descId
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
'aria-labelledby': titleId,
|
||||||
|
'aria-describedby': descId
|
||||||
}
|
}
|
||||||
: {}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
|
|||||||
Reference in New Issue
Block a user