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:
Dante
2026-03-11 08:30:03 +09:00
committed by GitHub
parent a786825093
commit d2792cfac6
3 changed files with 120 additions and 7 deletions

View File

@@ -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" />'
})
}

View File

@@ -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', () => {

View File

@@ -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