mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-12 16:40:05 +00:00
feat: add Storybook stories for Display components (#9702)
## Summary - Add Storybook stories for `WidgetImageCompare` (Default, WithBatchNavigation, SingleImageFallback, NoImages) - WidgetGalleria and ImagePreview stories are deferred pending PrimeVue removal ## Test plan - [x] `pnpm typecheck` passes - [x] `pnpm lint` passes - [x] Verified all stories render correctly in Storybook Figma ref: https://www.figma.com/design/vALUV83vIdBzEsTJAhQgXq/Comfy-Design-System?node-id=55-1536 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9702-feat-add-Storybook-stories-for-Display-components-31f6d73d365081e781faf3a8735aa3dc) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
|
||||
import type { ImageCompareValue } from './WidgetImageCompare.vue'
|
||||
import WidgetImageCompare from './WidgetImageCompare.vue'
|
||||
|
||||
function createSampleImage(label: string, fill: string): string {
|
||||
const svg =
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">` +
|
||||
`<rect width="512" height="512" fill="${fill}" />` +
|
||||
`<text x="50%" y="50%" fill="white" font-size="40"` +
|
||||
` text-anchor="middle" dominant-baseline="middle">` +
|
||||
`${label}</text></svg>`
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
||||
}
|
||||
|
||||
const SAMPLE_BEFORE = createSampleImage('Before', '#475569')
|
||||
const SAMPLE_AFTER = createSampleImage('After', '#0f766e')
|
||||
|
||||
const meta: Meta<typeof WidgetImageCompare> = {
|
||||
title: 'Components/Display/ImageCompare',
|
||||
component: WidgetImageCompare,
|
||||
tags: ['autodocs'],
|
||||
parameters: { layout: 'centered' },
|
||||
decorators: [
|
||||
(story) => ({
|
||||
components: { story },
|
||||
template: '<div class="w-88 h-80"><story /></div>'
|
||||
})
|
||||
]
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => ({
|
||||
components: { WidgetImageCompare },
|
||||
setup() {
|
||||
const widget = ref<SimplifiedWidget<ImageCompareValue>>({
|
||||
name: 'compare',
|
||||
type: 'IMAGE_COMPARE',
|
||||
value: {
|
||||
beforeImages: [SAMPLE_BEFORE],
|
||||
afterImages: [SAMPLE_AFTER]
|
||||
}
|
||||
})
|
||||
return { widget }
|
||||
},
|
||||
template: '<WidgetImageCompare :widget="widget" />'
|
||||
})
|
||||
}
|
||||
|
||||
export const WithBatchNavigation: Story = {
|
||||
render: () => ({
|
||||
components: { WidgetImageCompare },
|
||||
setup() {
|
||||
const widget = ref<SimplifiedWidget<ImageCompareValue>>({
|
||||
name: 'compare',
|
||||
type: 'IMAGE_COMPARE',
|
||||
value: {
|
||||
beforeImages: [SAMPLE_BEFORE, SAMPLE_AFTER],
|
||||
afterImages: [SAMPLE_AFTER, SAMPLE_BEFORE],
|
||||
beforeAlt: 'Before batch',
|
||||
afterAlt: 'After batch'
|
||||
}
|
||||
})
|
||||
return { widget }
|
||||
},
|
||||
template: '<WidgetImageCompare :widget="widget" />'
|
||||
})
|
||||
}
|
||||
|
||||
export const SingleImageFallback: Story = {
|
||||
render: () => ({
|
||||
components: { WidgetImageCompare },
|
||||
setup() {
|
||||
const widget = ref<SimplifiedWidget<string>>({
|
||||
name: 'compare',
|
||||
type: 'IMAGE_COMPARE',
|
||||
value: SAMPLE_BEFORE
|
||||
})
|
||||
return { widget }
|
||||
},
|
||||
template: '<WidgetImageCompare :widget="widget" />'
|
||||
})
|
||||
}
|
||||
|
||||
export const NoImages: Story = {
|
||||
render: () => ({
|
||||
components: { WidgetImageCompare },
|
||||
setup() {
|
||||
const widget = ref<SimplifiedWidget<ImageCompareValue>>({
|
||||
name: 'compare',
|
||||
type: 'IMAGE_COMPARE',
|
||||
value: {}
|
||||
})
|
||||
return { widget }
|
||||
},
|
||||
template: '<WidgetImageCompare :widget="widget" />'
|
||||
})
|
||||
}
|
||||
@@ -58,7 +58,7 @@ describe('WidgetImageCompare Display', () => {
|
||||
expect(images[1].attributes('src')).toBe('https://example.com/before.jpg')
|
||||
|
||||
images.forEach((img) => {
|
||||
expect(img.classes()).toContain('object-contain')
|
||||
expect(img.classes()).toContain('object-cover')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -290,7 +290,6 @@ describe('WidgetImageCompare Display', () => {
|
||||
|
||||
const slider = wrapper.find('[role="presentation"]')
|
||||
expect(slider.exists()).toBe(true)
|
||||
expect(slider.classes()).toContain('bg-white')
|
||||
})
|
||||
|
||||
it('does not render slider when no images', () => {
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
<div
|
||||
v-if="beforeImage || afterImage"
|
||||
ref="containerRef"
|
||||
class="relative min-h-0 flex-1"
|
||||
class="relative min-h-0 flex-1 overflow-hidden rounded-lg bg-node-component-surface py-4"
|
||||
>
|
||||
<img
|
||||
v-if="afterImage"
|
||||
:src="afterImage"
|
||||
:alt="afterAlt"
|
||||
draggable="false"
|
||||
class="size-full object-contain"
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
/>
|
||||
|
||||
<img
|
||||
@@ -41,12 +41,18 @@
|
||||
:src="beforeImage"
|
||||
:alt="beforeAlt"
|
||||
draggable="false"
|
||||
class="absolute inset-0 size-full object-contain"
|
||||
:style="{ clipPath: `inset(0 ${100 - sliderPosition}% 0 0)` }"
|
||||
class="absolute inset-0 size-full object-cover"
|
||||
:style="
|
||||
hasCompareImages
|
||||
? { clipPath: `inset(0 ${100 - sliderPosition}% 0 0)` }
|
||||
: undefined
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- Circular drag handle -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 z-10 w-0.5 bg-white shadow-md"
|
||||
v-if="hasCompareImages"
|
||||
class="pointer-events-none absolute top-1/2 z-10 size-6 -translate-1/2 rounded-full border-2 border-white bg-white/30 shadow-lg backdrop-blur-sm"
|
||||
:style="{ left: `${sliderPosition}%` }"
|
||||
role="presentation"
|
||||
/>
|
||||
@@ -142,6 +148,10 @@ const afterImage = computed(() => {
|
||||
return value?.afterImages?.[afterIndex.value] ?? ''
|
||||
})
|
||||
|
||||
const hasCompareImages = computed(() =>
|
||||
Boolean(beforeImage.value && afterImage.value)
|
||||
)
|
||||
|
||||
const beforeAlt = computed(() => {
|
||||
const value = props.widget.value
|
||||
return !isSingleImage(value) && value?.beforeAlt
|
||||
|
||||
Reference in New Issue
Block a user