diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 3d5761eff1..0c06fb3c93 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -35,6 +35,10 @@ "videoPreview": "Video preview - Use arrow keys to navigate between videos", "galleryImage": "Gallery image", "galleryThumbnail": "Gallery thumbnail", + "previousImage": "Previous image", + "nextImage": "Next image", + "switchToGridView": "Switch to grid view", + "switchToSingleView": "Switch to single view", "errorLoadingImage": "Error loading image", "errorLoadingVideo": "Error loading video", "failedToDownloadImage": "Failed to download image", diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.stories.ts b/src/renderer/extensions/vueNodes/components/ImagePreview.stories.ts new file mode 100644 index 0000000000..1e2796b0bc --- /dev/null +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.stories.ts @@ -0,0 +1,61 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import ImagePreview from './ImagePreview.vue' + +const SAMPLE_URLS = [ + 'https://picsum.photos/seed/preview1/800/600', + 'https://picsum.photos/seed/preview2/800/600', + 'https://picsum.photos/seed/preview3/800/600' +] + +const meta: Meta = { + title: 'Components/Display/ImagePreview', + component: ImagePreview, + tags: ['autodocs'], + parameters: { + layout: 'centered', + docs: { + description: { + component: + 'Node output image preview with navigation dots, keyboard controls, and hover action buttons (download, remove, edit/mask).' + } + } + }, + decorators: [ + (story) => ({ + components: { story }, + template: + '
' + }) + ] +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + imageUrls: [SAMPLE_URLS[0]] + } +} + +export const MultipleImages: Story = { + args: { + imageUrls: SAMPLE_URLS + } +} + +export const ErrorState: Story = { + args: { + imageUrls: ['https://invalid.example.com/no-image.png'] + } +} + +export const ManyImages: Story = { + args: { + imageUrls: Array.from( + { length: 8 }, + (_, i) => `https://picsum.photos/seed/many${i}/800/600` + ) + } +} diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.test.ts b/src/renderer/extensions/vueNodes/components/ImagePreview.test.ts index b781dc6fda..756e3ca8f0 100644 --- a/src/renderer/extensions/vueNodes/components/ImagePreview.test.ts +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.test.ts @@ -101,7 +101,7 @@ describe('ImagePreview', () => { it('shows navigation dots for multiple images', () => { const wrapper = mountImagePreview() - const navigationDots = wrapper.findAll('.w-2.h-2.rounded-full') + const navigationDots = wrapper.findAll('[aria-label*="View image"]') expect(navigationDots).toHaveLength(2) }) @@ -110,7 +110,7 @@ describe('ImagePreview', () => { imageUrls: [defaultProps.imageUrls[0]] }) - const navigationDots = wrapper.findAll('.w-2.h-2.rounded-full') + const navigationDots = wrapper.findAll('[aria-label*="View image"]') expect(navigationDots).toHaveLength(0) }) @@ -249,7 +249,7 @@ describe('ImagePreview', () => { ) // Click second navigation dot - const navigationDots = wrapper.findAll('.w-2.h-2.rounded-full') + const navigationDots = wrapper.findAll('[aria-label*="View image"]') await navigationDots[1].trigger('click') await nextTick() @@ -259,22 +259,22 @@ describe('ImagePreview', () => { expect(imgElement.attributes('src')).toBe(defaultProps.imageUrls[1]) }) - it('applies correct classes to navigation dots based on current image', async () => { + it('marks active navigation dot with aria-current', async () => { const wrapper = mountImagePreview() - const navigationDots = wrapper.findAll('.w-2.h-2.rounded-full') + const navigationDots = wrapper.findAll('[aria-label*="View image"]') - // First dot should be active (has bg-white class) - expect(navigationDots[0].classes()).toContain('bg-base-foreground') - expect(navigationDots[1].classes()).toContain('bg-base-foreground/50') + // First dot should be active + expect(navigationDots[0].attributes('aria-current')).toBe('true') + expect(navigationDots[1].attributes('aria-current')).toBeUndefined() // Switch to second image await navigationDots[1].trigger('click') await nextTick() // Second dot should now be active - expect(navigationDots[0].classes()).toContain('bg-base-foreground/50') - expect(navigationDots[1].classes()).toContain('bg-base-foreground') + expect(navigationDots[0].attributes('aria-current')).toBeUndefined() + expect(navigationDots[1].attributes('aria-current')).toBe('true') }) it('loads image without errors', async () => { @@ -301,7 +301,7 @@ describe('ImagePreview', () => { expect(wrapper.find('img').attributes('alt')).toBe('Node output 1') // Switch to second image - const navigationDots = wrapper.findAll('.w-2.h-2.rounded-full') + const navigationDots = wrapper.findAll('[aria-label*="View image"]') await navigationDots[1].trigger('click') await nextTick() @@ -326,7 +326,7 @@ describe('ImagePreview', () => { expect(wrapper.find('[aria-busy="true"]').exists()).toBe(false) // Click second navigation dot to cycle - const dots = wrapper.findAll('.w-2.h-2.rounded-full') + const dots = wrapper.findAll('[aria-label*="View image"]') await dots[1].trigger('click') await nextTick() diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue index 23da04810b..157d0328a7 100644 --- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue @@ -7,7 +7,7 @@ @@ -119,15 +124,16 @@