feat: replace PrimeVue Galleria/Skeleton with custom DisplayCarousel and ImagePreview (#9712)

## Summary
- Replace `primevue/galleria` with custom `DisplayCarousel` component
featuring Single (carousel) and Grid display modes
- Hover action buttons (mask, download, remove) appear on image
hover/focus
- Thumbnail strip with prev/next navigation; arrows at edges, thumbnails
centered
- Grid mode uses fixed 56px image tiles matching Figma spec
- Replace `primevue/skeleton` and `useToast()` in `ImagePreview` with
`Skeleton.vue` and `useToastStore()`
- Rename `WidgetGalleria` → `DisplayCarousel` across registry, stories,
and tests
- Add Storybook stories for both `DisplayCarousel` and `ImagePreview`
- Retain `WidgetGalleriaOriginal` with its own story for side-by-side
comparison

## Test plan
- [x] Unit tests pass (30 DisplayCarousel + 21 ImagePreview)
- [x] `pnpm typecheck` clean
- [x] `pnpm lint` clean
- [x] `pnpm knip` clean
- [x] Visual verification via Storybook: hover controls, nav, grid mode,
single/grid toggle
- [x] Manual Storybook check: Components/Display/DisplayCarousel,
Components/Display/ImagePreview


## screenshot
<img width="604" height="642" alt="스크린샷 2026-03-12 오후 2 01 51"
src="https://github.com/user-attachments/assets/94df3070-9910-470b-a8f5-5507433ef6e6"
/>
<img width="609" height="651" alt="스크린샷 2026-03-12 오후 2 04 47"
src="https://github.com/user-attachments/assets/3d9884b4-f1bd-4ef5-957a-c7cf7fdc04d8"
/>
<img width="729" height="681" alt="스크린샷 2026-03-12 오후 2 04 49"
src="https://github.com/user-attachments/assets/715f9367-17a3-4d7b-b81f-a7cd6bd446bf"
/>
<img width="534" height="460" alt="스크린샷 2026-03-12 오후 2 05 39"
src="https://github.com/user-attachments/assets/b810eee2-55cb-4dbd-aaca-6331527d13ca"
/>


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dante
2026-03-12 18:10:42 +09:00
committed by GitHub
parent 7c2c59b9fb
commit 8db6fb7733
7 changed files with 1121 additions and 28 deletions

View File

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