mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
feat: Improve MediaAssetCard design and add responsive sidebar footer (#6749)
## Summary Implements design feedback for the asset panel, improving visual hierarchy, contrast, and responsiveness based on design tokens update. ## Changes ### 🎨 Design System Updates (style.css) - **New tokens for MediaAssetCard states:** - `--modal-card-background-hovered`: Hover state background - `--modal-card-border-highlighted`: Selected state border color - **Updated tag contrast:** - Light mode: `smoke-200` → `smoke-400` - Dark mode: `charcoal-200` → `ash-800` - **Registered tokens in Tailwind** via `@theme inline` for proper class generation ### 🖼️ MediaAssetCard Improvements - **Added tooltips** to all interactive buttons: - Zoom button: "Inspect" - More button: "More options" - Output count button: "See more outputs" - **Fixed tooltip event conflicts** by wrapping buttons in tooltip divs - **Updated hover/selected states:** - Hover: Uses `--modal-card-background-hovered` for subtle highlight - Selected: Uses `--modal-card-border-highlighted` for border only (no background) - **Updated placeholder background** to use `--modal-card-placeholder-background` - **Tag styling:** Changed from `variant="light"` to `variant="gray"` for better contrast ### 📦 SquareChip Component - **Added `gray` variant** that uses `--modal-card-tag-background` token - Maintains consistency with design system tokens ### 📱 AssetsSidebarTab Responsive Footer - **Responsive button display:** - Width > 350px: Shows icon + text buttons - Width ≤ 350px: Shows icon-only buttons - **Text alignment:** Left-aligns selection count text in compact mode - **Uses `useResizeObserver`** for automatic width detection ### 🌐 Internationalization - Added new i18n keys for tooltips: - `mediaAsset.actions.inspect` - `mediaAsset.actions.more` - `mediaAsset.actions.seeMoreOutputs` ### 🔧 Minor Fixes - **Media3DTop:** Improved text size and icon color for better visual hierarchy ## Visual Changes - **Increased contrast** for asset card tags (more visible in both themes) - **Hover state** now provides clear visual feedback - **Selected state** uses border highlight instead of background fill - **Sidebar footer** gracefully adapts to narrow widths ## Related - Addresses feedback from: https://www.notion.so/comfy-org/Asset-panel-feedback-2aa6d73d3650800baacaf739a49360b3 - Design token updates by @Alex Tov ## Test Plan - [ ] Verify asset card hover states in both light and dark themes - [ ] Verify asset card selected states show highlighted border - [ ] Test tooltips on all MediaAssetCard buttons - [ ] Resize sidebar to < 350px and verify footer shows icon-only buttons - [ ] Resize sidebar to > 350px and verify footer shows icon + text buttons - [ ] Verify tag contrast improvement in both themes - [ ] Test 3D asset placeholder appearance ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6749-feat-Improve-MediaAssetCard-design-and-add-responsive-sidebar-footer-2b06d73d365081019b90e110df2f1ae8) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -268,12 +268,13 @@
|
||||
--palette-interface-button-hover-surface: color-mix(in srgb, var(--interface-panel-surface) 82%, var(--contrast-mix-color));
|
||||
|
||||
--modal-card-background: var(--secondary-background);
|
||||
--modal-card-background-hovered: var(--secondary-background-hover);
|
||||
--modal-card-border-highlighted: var(--secondary-background-selected);
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
--modal-card-placeholder-background: var(--color-smoke-600);
|
||||
--modal-card-tag-background: var(--color-smoke-200);
|
||||
--modal-card-tag-background: var(--color-smoke-400);
|
||||
--modal-card-tag-foreground: var(--base-foreground);
|
||||
--modal-panel-background: var(--color-white);
|
||||
|
||||
}
|
||||
|
||||
.dark-theme {
|
||||
@@ -377,9 +378,11 @@
|
||||
--component-node-widget-background-highlighted: var(--color-graphite-400);
|
||||
|
||||
--modal-card-background: var(--secondary-background);
|
||||
--modal-card-background-hovered: var(--secondary-background-hover);
|
||||
--modal-card-border-highlighted: var(--color-ash-400);
|
||||
--modal-card-button-surface: var(--color-charcoal-300);
|
||||
--modal-card-placeholder-background: var(--secondary-background);
|
||||
--modal-card-tag-background: var(--color-charcoal-200);
|
||||
--modal-card-tag-background: var(--color-ash-800);
|
||||
--modal-card-tag-foreground: var(--base-foreground);
|
||||
--modal-panel-background: var(--color-charcoal-600);
|
||||
|
||||
@@ -395,12 +398,14 @@
|
||||
--color-subscription-button-gradient: var(--subscription-button-gradient);
|
||||
|
||||
--color-modal-card-background: var(--modal-card-background);
|
||||
--color-modal-card-background-hovered: var(--modal-card-background-hovered);
|
||||
--color-modal-card-border-highlighted: var(--modal-card-border-highlighted);
|
||||
--color-modal-card-button-surface: var(--modal-card-button-surface);
|
||||
--color-modal-card-placeholder-background: var(--modal-card-placeholder-background);
|
||||
--color-modal-card-tag-background: var(--modal-card-tag-background);
|
||||
--color-modal-card-tag-foreground: var(--modal-card-tag-foreground);
|
||||
--color-modal-panel-background: var(--modal-panel-background);
|
||||
|
||||
|
||||
--color-dialog-surface: var(--dialog-surface);
|
||||
--color-interface-menu-component-surface-hovered: var(
|
||||
--interface-menu-component-surface-hovered
|
||||
|
||||
@@ -11,7 +11,7 @@ import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { label, variant = 'dark' } = defineProps<{
|
||||
label: string
|
||||
variant?: 'dark' | 'light'
|
||||
variant?: 'dark' | 'light' | 'gray'
|
||||
}>()
|
||||
|
||||
const baseClasses =
|
||||
@@ -19,7 +19,10 @@ const baseClasses =
|
||||
|
||||
const variantStyles = {
|
||||
dark: 'bg-zinc-500/40 text-white/90',
|
||||
light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground')
|
||||
light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground'),
|
||||
gray: cn(
|
||||
'backdrop-blur-[2px] bg-modal-card-tag-background text-base-foreground'
|
||||
)
|
||||
}
|
||||
|
||||
const chipClasses = computed(() => {
|
||||
|
||||
@@ -97,43 +97,62 @@
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="hasSelection"
|
||||
ref="footerRef"
|
||||
class="flex gap-1 h-18 w-full items-center justify-between"
|
||||
>
|
||||
<div ref="selectionCountButtonRef" class="flex-1 pl-4">
|
||||
<TextButton
|
||||
:label="
|
||||
isHoveringSelectionCount
|
||||
? $t('mediaAsset.selection.deselectAll')
|
||||
: $t('mediaAsset.selection.selectedCount', {
|
||||
count: totalOutputCount
|
||||
})
|
||||
"
|
||||
type="transparent"
|
||||
@click="handleDeselectAll"
|
||||
/>
|
||||
<div class="flex-1 pl-4">
|
||||
<div ref="selectionCountButtonRef" class="inline-flex w-48">
|
||||
<TextButton
|
||||
:label="
|
||||
isHoveringSelectionCount
|
||||
? $t('mediaAsset.selection.deselectAll')
|
||||
: $t('mediaAsset.selection.selectedCount', {
|
||||
count: totalOutputCount
|
||||
})
|
||||
"
|
||||
type="transparent"
|
||||
:class="isCompact ? 'text-left' : ''"
|
||||
@click="handleDeselectAll"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 pr-4">
|
||||
<IconTextButton
|
||||
v-if="shouldShowDeleteButton"
|
||||
:label="$t('mediaAsset.selection.deleteSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDeleteSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<template v-if="isCompact">
|
||||
<!-- Compact mode: Icon only -->
|
||||
<IconButton
|
||||
v-if="shouldShowDeleteButton"
|
||||
@click="handleDeleteSelected"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
:label="$t('mediaAsset.selection.downloadSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDownloadSelected"
|
||||
>
|
||||
<template #icon>
|
||||
</IconButton>
|
||||
<IconButton @click="handleDownloadSelected">
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</IconButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Normal mode: Icon + Text -->
|
||||
<IconTextButton
|
||||
v-if="shouldShowDeleteButton"
|
||||
:label="$t('mediaAsset.selection.deleteSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDeleteSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
:label="$t('mediaAsset.selection.downloadSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDownloadSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -145,11 +164,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn, useElementHover } from '@vueuse/core'
|
||||
import { useDebounceFn, useElementHover, useResizeObserver } from '@vueuse/core'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import TextButton from '@/components/button/TextButton.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
@@ -221,6 +241,22 @@ const {
|
||||
|
||||
const { downloadMultipleAssets, deleteMultipleAssets } = useMediaAssetActions()
|
||||
|
||||
// Footer responsive behavior
|
||||
const footerRef = ref<HTMLElement | null>(null)
|
||||
const footerWidth = ref(0)
|
||||
|
||||
// Track footer width changes
|
||||
useResizeObserver(footerRef, (entries) => {
|
||||
const entry = entries[0]
|
||||
footerWidth.value = entry.contentRect.width
|
||||
})
|
||||
|
||||
// Determine if we should show compact mode (icon only)
|
||||
// Threshold: 350px or less shows icon only
|
||||
const isCompact = computed(
|
||||
() => footerWidth.value > 0 && footerWidth.value <= 350
|
||||
)
|
||||
|
||||
// Hover state for selection count button
|
||||
const selectionCountButtonRef = ref<HTMLElement | null>(null)
|
||||
const isHoveringSelectionCount = useElementHover(selectionCountButtonRef)
|
||||
|
||||
@@ -2102,6 +2102,11 @@
|
||||
"assetDeletedSuccessfully": "Asset deleted successfully",
|
||||
"deletingImportedFilesCloudOnly": "Deleting imported files is only supported in cloud version",
|
||||
"failedToDeleteAsset": "Failed to delete asset",
|
||||
"actions": {
|
||||
"inspect": "Inspect",
|
||||
"more": "More options",
|
||||
"seeMoreOutputs": "See more outputs"
|
||||
},
|
||||
"jobIdToast": {
|
||||
"jobIdCopied": "Job ID copied to clipboard",
|
||||
"jobIdCopyFailed": "Failed to copy Job ID",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<div
|
||||
class="flex size-full flex-col items-center justify-center gap-2 bg-modal-card-placeholder-background transition-transform duration-300 group-hover:scale-105 group-data-[selected=true]:scale-105"
|
||||
>
|
||||
<i class="icon-[lucide--box] text-3xl text-base-foreground" />
|
||||
<span class="text-base-foreground">{{
|
||||
<i class="icon-[lucide--box] text-3xl text-muted-foreground" />
|
||||
<span class="text-sm text-base-foreground">{{
|
||||
$t('assetBrowser.media.threeDModelPlaceholder')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<!-- Loading State -->
|
||||
<template v-if="loading">
|
||||
<div
|
||||
class="size-full animate-pulse rounded-lg bg-modal-card-button-surface"
|
||||
class="size-full animate-pulse rounded-lg bg-modal-card-placeholder-background"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -51,44 +51,51 @@
|
||||
<div v-if="showStaticChips" class="flex flex-wrap items-center gap-1">
|
||||
<SquareChip
|
||||
v-if="formattedDuration"
|
||||
variant="light"
|
||||
variant="gray"
|
||||
:label="formattedDuration"
|
||||
/>
|
||||
<SquareChip v-if="fileFormat" variant="light" :label="fileFormat" />
|
||||
<SquareChip v-if="fileFormat" variant="gray" :label="fileFormat" />
|
||||
</div>
|
||||
|
||||
<!-- Media actions - show on hover or when playing -->
|
||||
<IconGroup v-else-if="showActionsOverlay">
|
||||
<IconButton
|
||||
size="sm"
|
||||
@click.stop="handleZoomClick"
|
||||
@mouseenter="handleOverlayMouseEnter"
|
||||
@mouseleave="handleOverlayMouseLeave"
|
||||
>
|
||||
<i class="icon-[lucide--zoom-in] size-4" />
|
||||
</IconButton>
|
||||
<MoreButton
|
||||
size="sm"
|
||||
@menu-opened="isMenuOpen = true"
|
||||
@menu-closed="isMenuOpen = false"
|
||||
@mouseenter="handleOverlayMouseEnter"
|
||||
@mouseleave="handleOverlayMouseLeave"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetMoreMenu
|
||||
:close="close"
|
||||
:show-delete-button="showDeleteButton"
|
||||
@inspect="handleZoomClick"
|
||||
@asset-deleted="handleAssetDelete"
|
||||
/>
|
||||
</template>
|
||||
</MoreButton>
|
||||
<div v-tooltip.top="$t('mediaAsset.actions.inspect')">
|
||||
<IconButton
|
||||
size="sm"
|
||||
@click.stop="handleZoomClick"
|
||||
@mouseenter="handleOverlayMouseEnter"
|
||||
@mouseleave="handleOverlayMouseLeave"
|
||||
>
|
||||
<i class="icon-[lucide--zoom-in] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div v-tooltip.top="$t('mediaAsset.actions.more')">
|
||||
<MoreButton
|
||||
size="sm"
|
||||
@menu-opened="isMenuOpen = true"
|
||||
@menu-closed="isMenuOpen = false"
|
||||
@mouseenter="handleOverlayMouseEnter"
|
||||
@mouseleave="handleOverlayMouseLeave"
|
||||
>
|
||||
<template #default="{ close }">
|
||||
<MediaAssetMoreMenu
|
||||
:close="close"
|
||||
:show-delete-button="showDeleteButton"
|
||||
@inspect="handleZoomClick"
|
||||
@asset-deleted="handleAssetDelete"
|
||||
/>
|
||||
</template>
|
||||
</MoreButton>
|
||||
</div>
|
||||
</IconGroup>
|
||||
</template>
|
||||
|
||||
<!-- Output count (top-right) -->
|
||||
<template v-if="showOutputCount" #top-right>
|
||||
<IconTextButton
|
||||
v-tooltip.top.pt:pointer-events-none="
|
||||
$t('mediaAsset.actions.seeMoreOutputs')
|
||||
"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
:label="String(outputCount)"
|
||||
@@ -251,8 +258,8 @@ const containerClasses = computed(() =>
|
||||
cn(
|
||||
'gap-1 select-none group',
|
||||
selected
|
||||
? 'ring-3 ring-inset ring-base-foreground bg-modal-card-background'
|
||||
: 'hover:bg-modal-card-background'
|
||||
? 'ring-3 ring-inset ring-modal-card-border-highlighted'
|
||||
: 'hover:bg-modal-card-background-hovered'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user