Add MediaAssetCard presentation components (#5878)

## Summary

Implements a comprehensive media asset card component system for the
Asset Manager sidebar, enabling display and interaction with various
media types (images, videos, audio, and 3D models).

## Changes

### New Components
- **MediaAssetCard**: Main card component for displaying media assets
- **Media type-specific components**: Specialized display logic for each
media type
  - MediaImageTop/Bottom
  - MediaVideoTop/Bottom  
  - MediaAudioTop/Bottom
  - Media3DTop/Bottom
- **MediaAssetActions**: Top-left action buttons (delete, download, more
options)
- **MediaAssetMoreMenu**: Dropdown menu for additional actions
- **SquareChip**: Chip component for displaying duration and file format
with dark/light variants
- **MediaAssetButtonDivider**: Visual separator for button groups

### Features
- **Video playback**: Autoplay with native video controls
  - Dynamic duration chip positioning based on control visibility
  - Hides overlays when video is playing
- **Audio playback**: Audio icon with HTML5 audio element
  - Duration chip with consistent positioning
- **3D model support**: Icon display for 3D assets
- **Selection state**: Proper hover and selected state handling with CSS
priority fixes

### Architecture Improvements
- **Domain-Driven Design structure**: Organized under
`src/platform/mediaAsset/` following DDD principles
- **Provide/Inject pattern**: Eliminates props drilling with
MediaAssetKey InjectionKey
- **Composable pattern**: `useMediaAssetActions` manages all action
handlers
- **Type safety**: Comprehensive TypeScript types for media assets and
actions

### UI/UX Enhancements
- **CardTop component**: Added custom class props for slot positioning
- **SquareChip component**: Backdrop blur effects with variant system
- **Lazy loading**: Image optimization with LazyImage component
- **Responsive states**: Loading, selected, and hover states

### Utilities
- **formatDuration**: Converts milliseconds to human-readable format
(45s, 1m 23s, 1h 2m)

## Testing
- Comprehensive Storybook stories for all media types
- Grid layout examples
- Loading and selected state demonstrations

## File Structure
```
src/platform/assets/
├── components/
│   ├── MediaAssetCard.vue
│   ├── MediaAssetCard.stories.ts
│   ├── MediaAssetActions.vue
│   ├── MediaAssetMoreMenu.vue
│   ├── MediaAssetButtonDivider.vue
│   ├── MediaImageTop.vue
│   ├── MediaImageBottom.vue
│   ├── MediaVideoTop.vue
│   ├── MediaVideoBottom.vue
│   ├── MediaAudioTop.vue
│   ├── MediaAudioBottom.vue
│   ├── Media3DTop.vue
│   └── Media3DBottom.vue
├── composables/
│   └── useMediaAssetActions.ts
└── schemas/
    └── mediaAssetSchema.ts
```

## Screenshots

[media_asset_record.webm](https://github.com/user-attachments/assets/d13b5cc0-a262-4850-bb81-ca1daa0dd969)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Jin Yi
2025-10-12 06:39:04 +09:00
committed by GitHub
parent bb83b0107c
commit 9c0b3c4f7d
27 changed files with 1655 additions and 62 deletions

View File

@@ -12,6 +12,7 @@ const iconGroupClasses = cn(
'outline-hidden border-none p-0 rounded-lg',
'bg-white dark-theme:bg-zinc-700',
'text-neutral-950 dark-theme:text-white',
'transition-all duration-200',
'cursor-pointer'
)
</script>

View File

@@ -1,7 +1,8 @@
<template>
<div class="relative inline-flex items-center">
<IconButton @click="toggle">
<i class="icon-[lucide--more-vertical] text-sm" />
<IconButton :size="size" :type="type" @click="toggle">
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
<i v-else class="icon-[lucide--more-vertical] text-sm" />
</IconButton>
<Popover
@@ -13,6 +14,8 @@
:close-on-escape="true"
unstyled
:pt="pt"
@show="$emit('menuOpened')"
@hide="$emit('menuClosed')"
>
<div class="flex min-w-40 flex-col gap-2 p-2">
<slot :close="hide" />
@@ -25,12 +28,28 @@
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
import IconButton from './IconButton.vue'
interface MoreButtonProps extends BaseButtonProps {
isVertical?: boolean
}
const popover = ref<InstanceType<typeof Popover>>()
const {
size = 'md',
type = 'secondary',
isVertical = false
} = defineProps<MoreButtonProps>()
defineEmits<{
menuOpened: []
menuClosed: []
}>()
const toggle = (event: Event) => {
popover.value?.toggle(event)
}
@@ -45,7 +64,7 @@ const pt = computed(() => ({
},
content: {
class: cn(
'mt-2 rounded-lg',
'mt-1 rounded-lg',
'bg-white dark-theme:bg-zinc-800',
'text-neutral dark-theme:text-white',
'shadow-lg',

View File

@@ -11,7 +11,15 @@ import CardTop from './CardTop.vue'
interface CardStoryArgs {
// CardContainer props
containerRatio: 'square' | 'portrait' | 'tallPortrait'
containerSize: 'mini' | 'compact' | 'regular' | 'portrait' | 'tall'
variant: 'default' | 'ghost' | 'outline'
rounded: 'none' | 'sm' | 'lg' | 'xl'
customAspectRatio?: string
hasBorder: boolean
hasBackground: boolean
hasShadow: boolean
hasCursor: boolean
customClass: string
maxWidth: number
minWidth: number
@@ -44,10 +52,44 @@ interface CardStoryArgs {
const meta: Meta<CardStoryArgs> = {
title: 'Components/Card/Card',
argTypes: {
containerRatio: {
containerSize: {
control: 'select',
options: ['square', 'portrait', 'tallPortrait'],
description: 'Card container aspect ratio'
options: ['mini', 'compact', 'regular', 'portrait', 'tall'],
description: 'Card container size preset'
},
variant: {
control: 'select',
options: ['default', 'ghost', 'outline'],
description: 'Card visual variant'
},
rounded: {
control: 'select',
options: ['none', 'sm', 'lg', 'xl'],
description: 'Border radius size'
},
customAspectRatio: {
control: 'text',
description: 'Custom aspect ratio (e.g., "16/9")'
},
hasBorder: {
control: 'boolean',
description: 'Add border styling'
},
hasBackground: {
control: 'boolean',
description: 'Add background styling'
},
hasShadow: {
control: 'boolean',
description: 'Add shadow styling'
},
hasCursor: {
control: 'boolean',
description: 'Add cursor pointer'
},
customClass: {
control: 'text',
description: 'Additional custom CSS classes'
},
topRatio: {
control: 'select',
@@ -149,8 +191,15 @@ const createCardTemplate = (args: CardStoryArgs) => ({
template: `
<div class="min-h-screen">
<CardContainer
:ratio="args.containerRatio"
class="max-w-[320px] mx-auto"
:size="args.containerSize"
:variant="args.variant"
:rounded="args.rounded"
:custom-aspect-ratio="args.customAspectRatio"
:has-border="args.hasBorder"
:has-background="args.hasBackground"
:has-shadow="args.hasShadow"
:has-cursor="args.hasCursor"
:class="args.customClass || 'max-w-[320px] mx-auto'"
>
<template #top>
<CardTop :ratio="args.topRatio">
@@ -205,7 +254,7 @@ const createCardTemplate = (args: CardStoryArgs) => ({
</template>
<template #bottom>
<CardBottom class="p-3 bg-neutral-100">
<CardBottom>
<CardTitle v-if="args.showTitle">{{ args.title }}</CardTitle>
<CardDescription v-if="args.showDescription">{{ args.description }}</CardDescription>
</CardBottom>
@@ -218,7 +267,15 @@ const createCardTemplate = (args: CardStoryArgs) => ({
export const Default: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'portrait',
containerSize: 'portrait',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: false,
showTopRight: true,
@@ -243,7 +300,15 @@ export const Default: Story = {
export const SquareCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'square',
containerSize: 'regular',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'landscape',
showTopLeft: false,
showTopRight: true,
@@ -268,7 +333,15 @@ export const SquareCard: Story = {
export const TallPortraitCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'tallPortrait',
containerSize: 'tall',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: true,
showTopRight: true,
@@ -293,7 +366,15 @@ export const TallPortraitCard: Story = {
export const ImageCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'portrait',
containerSize: 'portrait',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: false,
showTopRight: true,
@@ -314,10 +395,50 @@ export const ImageCard: Story = {
}
}
export const MiniCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'mini',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: false,
title: 'Mini Asset',
description: '',
backgroundColor: '#06b6d4',
showImage: false,
imageUrl: '',
tags: ['Asset'],
showFileSize: true,
fileSize: '124 KB',
showFileType: false,
fileType: ''
}
}
export const MinimalCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'square',
containerSize: 'regular',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'landscape',
showTopLeft: false,
showTopRight: false,
@@ -338,10 +459,209 @@ export const MinimalCard: Story = {
}
}
export const GhostVariant: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'compact',
variant: 'ghost',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: true,
title: 'Workflow Template',
description: 'Ghost variant for workflow templates',
backgroundColor: '#10b981',
showImage: false,
imageUrl: '',
tags: ['Template'],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const OutlineVariant: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'regular',
variant: 'outline',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'landscape',
showTopLeft: false,
showTopRight: true,
showBottomLeft: false,
showBottomRight: false,
showTitle: true,
showDescription: true,
title: 'Outline Card',
description: 'Card with outline variant styling',
backgroundColor: '#f59e0b',
showImage: false,
imageUrl: '',
tags: [],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const CustomAspectRatio: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'regular',
variant: 'default',
customAspectRatio: '16/9',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'landscape',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: true,
showTitle: true,
showDescription: false,
title: 'Wide Format Card',
description: '',
backgroundColor: '#8b5cf6',
showImage: false,
imageUrl: '',
tags: ['Wide'],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const RoundedNone: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'regular',
variant: 'default',
rounded: 'none',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: false,
showTitle: true,
showDescription: true,
title: 'Sharp Corners',
description: 'Card with no border radius',
backgroundColor: '#dc2626',
showImage: false,
imageUrl: '',
tags: [],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const RoundedXL: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'regular',
variant: 'default',
rounded: 'xl',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: false,
showTitle: true,
showDescription: true,
title: 'Extra Rounded',
description: 'Card with extra large border radius',
backgroundColor: '#059669',
showImage: false,
imageUrl: '',
tags: [],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const NoStylesCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerSize: 'regular',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: false,
hasBackground: false,
hasShadow: false,
hasCursor: true,
customClass: 'bg-gradient-to-br from-blue-500 to-purple-600',
topRatio: 'square',
showTopLeft: false,
showTopRight: false,
showBottomLeft: false,
showBottomRight: false,
showTitle: true,
showDescription: true,
title: 'Custom Styled Card',
description: 'Card with all default styles removed and custom gradient',
backgroundColor: 'transparent',
showImage: false,
imageUrl: '',
tags: [],
showFileSize: false,
fileSize: '',
showFileType: false,
fileType: ''
}
}
export const FullFeaturedCard: Story = {
render: (args: CardStoryArgs) => createCardTemplate(args),
args: {
containerRatio: 'tallPortrait',
containerSize: 'tall',
variant: 'default',
rounded: 'lg',
customAspectRatio: '',
hasBorder: true,
hasBackground: true,
hasShadow: true,
hasCursor: true,
customClass: '',
topRatio: 'square',
showTopLeft: true,
showTopRight: true,

View File

@@ -8,26 +8,78 @@
<script setup lang="ts">
import { computed } from 'vue'
const { ratio = 'square', type } = defineProps<{
ratio?: 'smallSquare' | 'square' | 'portrait' | 'tallPortrait'
type?: string
import { cn } from '@/utils/tailwindUtil'
const {
size = 'regular',
variant = 'default',
rounded = 'md',
customAspectRatio,
hasBorder = true,
hasBackground = true,
hasShadow = true,
hasCursor = true,
class: customClass = ''
} = defineProps<{
size?: 'mini' | 'compact' | 'regular' | 'portrait' | 'tall'
variant?: 'default' | 'ghost' | 'outline'
rounded?: 'none' | 'md' | 'lg' | 'xl'
customAspectRatio?: string
hasBorder?: boolean
hasBackground?: boolean
hasShadow?: boolean
hasCursor?: boolean
class?: string
}>()
// Base structure classes
const structureClasses = 'flex flex-col overflow-hidden'
// Rounded corners
const roundedClasses = {
none: 'rounded-none',
md: 'rounded',
lg: 'rounded-lg',
xl: 'rounded-xl'
} as const
const containerClasses = computed(() => {
const baseClasses =
'cursor-pointer flex flex-col bg-white dark-theme:bg-zinc-800 rounded-lg shadow-sm border border-zinc-200 dark-theme:border-zinc-700 overflow-hidden'
if (type === 'workflow-template-card') {
return `cursor-pointer p-2 flex flex-col hover:bg-white dark-theme:hover:bg-zinc-800 rounded-lg transition-background duration-200 ease-in-out`
// Variant styles
const variantClasses = {
default: cn(
hasBackground && 'bg-white dark-theme:bg-zinc-800',
hasBorder && 'border border-zinc-200 dark-theme:border-zinc-700',
hasShadow && 'shadow-sm',
hasCursor && 'cursor-pointer'
),
ghost: cn(
hasCursor && 'cursor-pointer',
'p-2 transition-colors duration-200'
),
outline: cn(
hasBorder && 'border-2 border-zinc-300 dark-theme:border-zinc-600',
hasCursor && 'cursor-pointer',
'hover:border-zinc-400 dark-theme:hover:border-zinc-500 transition-colors'
)
}
const ratioClasses = {
smallSquare: 'aspect-240/311',
square: 'aspect-256/308',
portrait: 'aspect-256/325',
tallPortrait: 'aspect-256/353'
}
// Size/aspect ratio
const aspectRatio = customAspectRatio
? `aspect-[${customAspectRatio}]`
: {
mini: 'aspect-100/120',
compact: 'aspect-240/311',
regular: 'aspect-256/308',
portrait: 'aspect-256/325',
tall: 'aspect-256/353'
}[size]
return `${baseClasses} ${ratioClasses[ratio]}`
return cn(
structureClasses,
roundedClasses[rounded],
variantClasses[variant],
aspectRatio,
customClass
)
})
</script>

View File

@@ -2,31 +2,27 @@
<div :class="topStyle">
<slot class="absolute top-0 left-0 h-full w-full"></slot>
<div
v-if="slots['top-left']"
class="absolute top-2 left-2 flex flex-wrap justify-start gap-2"
>
<div v-if="slots['top-left']" :class="slotClasses['top-left']">
<slot name="top-left"></slot>
</div>
<div
v-if="slots['top-right']"
class="absolute top-2 right-2 flex flex-wrap justify-end gap-2"
>
<div v-if="slots['top-right']" :class="slotClasses['top-right']">
<slot name="top-right"></slot>
</div>
<div
v-if="slots['bottom-left']"
class="absolute bottom-2 left-2 flex flex-wrap justify-start gap-2"
>
<div v-if="slots['center-left']" :class="slotClasses['center-left']">
<slot name="center-left"></slot>
</div>
<div v-if="slots['center-right']" :class="slotClasses['center-right']">
<slot name="center-right"></slot>
</div>
<div v-if="slots['bottom-left']" :class="slotClasses['bottom-left']">
<slot name="bottom-left"></slot>
</div>
<div
v-if="slots['bottom-right']"
class="absolute right-2 bottom-2 flex flex-wrap justify-end gap-2"
>
<div v-if="slots['bottom-right']" :class="slotClasses['bottom-right']">
<slot name="bottom-right"></slot>
</div>
</div>
@@ -35,10 +31,26 @@
<script setup lang="ts">
import { computed, useSlots } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const slots = useSlots()
const { ratio = 'square' } = defineProps<{
const {
ratio = 'square',
topLeftClass,
topRightClass,
centerLeftClass,
centerRightClass,
bottomLeftClass,
bottomRightClass
} = defineProps<{
ratio?: 'square' | 'landscape'
topLeftClass?: string
topRightClass?: string
centerLeftClass?: string
centerRightClass?: string
bottomLeftClass?: string
bottomRightClass?: string
}>()
const topStyle = computed(() => {
@@ -51,4 +63,26 @@ const topStyle = computed(() => {
return `${baseClasses} ${ratioClasses[ratio]}`
})
// Get default classes for each slot position
const defaultSlotClasses = {
'top-left': 'absolute top-2 left-2 flex flex-wrap justify-start gap-2',
'top-right': 'absolute top-2 right-2 flex flex-wrap justify-end gap-2',
'center-left':
'absolute top-1/2 left-2 flex -translate-y-1/2 flex-wrap justify-start gap-2',
'center-right':
'absolute top-1/2 right-2 flex -translate-y-1/2 flex-wrap justify-end gap-2',
'bottom-left': 'absolute bottom-2 left-2 flex flex-wrap justify-start gap-2',
'bottom-right': 'absolute right-2 bottom-2 flex flex-wrap justify-end gap-2'
}
// Compute all slot classes once and cache them
const slotClasses = computed(() => ({
'top-left': cn(defaultSlotClasses['top-left'], topLeftClass),
'top-right': cn(defaultSlotClasses['top-right'], topRightClass),
'center-left': cn(defaultSlotClasses['center-left'], centerLeftClass),
'center-right': cn(defaultSlotClasses['center-right'], centerRightClass),
'bottom-left': cn(defaultSlotClasses['bottom-left'], bottomLeftClass),
'bottom-right': cn(defaultSlotClasses['bottom-right'], bottomRightClass)
}))
</script>

View File

@@ -1,13 +1,28 @@
<template>
<div
class="inline-flex shrink-0 items-center justify-center gap-1 rounded bg-[#D9D9D966]/40 px-2 py-1 text-xs font-bold text-white/90"
>
<slot name="icon" class="text-xs text-white/90"></slot>
<div :class="chipClasses">
<slot name="icon"></slot>
<span>{{ label }}</span>
</div>
</template>
<script setup lang="ts">
const { label } = defineProps<{
import { computed } from 'vue'
import { cn } from '@/utils/tailwindUtil'
const { label, variant = 'dark' } = defineProps<{
label: string
variant?: 'dark' | 'light'
}>()
const baseClasses =
'inline-flex shrink-0 items-center justify-center gap-1 rounded px-2 py-1 text-xs font-bold'
const variantStyles = {
dark: 'bg-zinc-500/40 text-white/90',
light: 'backdrop-blur-[2px] bg-white/50 text-zinc-900 dark-theme:text-white'
}
const chipClasses = computed(() => {
return cn(baseClasses, variantStyles[variant])
})
</script>

View File

@@ -2,6 +2,7 @@
<div
ref="containerRef"
class="relative flex h-full w-full items-center justify-center overflow-hidden"
:class="containerClass"
>
<Skeleton
v-if="!isImageLoaded"
@@ -41,17 +42,20 @@ import { computed, onUnmounted, ref, watch } from 'vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
import { useMediaCache } from '@/services/mediaCacheService'
import type { ClassValue } from '@/utils/tailwindUtil'
const {
src,
alt = '',
containerClass = '',
imageClass = '',
imageStyle,
rootMargin = '300px'
} = defineProps<{
src: string
alt?: string
imageClass?: string | string[] | Record<string, boolean>
containerClass?: ClassValue
imageClass?: ClassValue
imageStyle?: Record<string, any>
rootMargin?: string
}>()

View File

@@ -141,8 +141,10 @@
<CardContainer
v-for="n in isLoading ? 12 : 0"
:key="`initial-skeleton-${n}`"
ratio="smallSquare"
type="workflow-template-card"
size="compact"
variant="ghost"
rounded="lg"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
>
<template #top>
<CardTop ratio="landscape">
@@ -172,9 +174,11 @@
v-for="template in isLoading ? [] : displayTemplates"
:key="template.name"
ref="cardRefs"
ratio="smallSquare"
type="workflow-template-card"
size="compact"
variant="ghost"
rounded="lg"
:data-testid="`template-workflow-${template.name}`"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
@mouseenter="hoveredTemplate = template.name"
@mouseleave="hoveredTemplate = null"
@click="onLoadWorkflow(template)"
@@ -316,8 +320,10 @@
<CardContainer
v-for="n in isLoadingMore ? 6 : 0"
:key="`skeleton-${n}`"
ratio="smallSquare"
type="workflow-template-card"
size="compact"
variant="ghost"
rounded="lg"
class="hover:bg-white dark-theme:hover:bg-zinc-800"
>
<template #top>
<CardTop ratio="square">

View File

@@ -88,7 +88,7 @@
<template #content>
<!-- Card Examples -->
<div :style="gridStyle">
<CardContainer v-for="i in 100" :key="i" ratio="square">
<CardContainer v-for="i in 100" :key="i" size="regular">
<template #top>
<CardTop ratio="landscape">
<template #default>