From 396d4e941fb7b33190fe322644e065842b2bb353 Mon Sep 17 00:00:00 2001 From: Matt Edholm Date: Wed, 6 May 2026 18:45:02 -0400 Subject: [PATCH] feat(home): replace horizontal carousel with vertical scroll-snap stack For multi-frame setups, switch from side-swipe carousel + dot indicators to a vertical scroll-snap stack of full-size cards. Each frame gets its own page-height slide; flicking up/down moves between frames with the same snap-stop feel as the horizontal version. Removes ~30 lines of carousel scroll-tracking JS and the dot navigation. Single-frame layout unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/test/views/HomeView.test.ts | 53 ++----- frontend/src/views/HomeView.vue | 134 +++++------------- ...XcLrct9.js => BaseBottomSheet-DYbXyS7e.js} | 2 +- ...r-MIsIaweQ.js => DevicePicker-kY4qiZhO.js} | 2 +- public/build/assets/HomeView-B-yz7GFB.js | 1 - ...iew-hc6H9m3_.css => HomeView-BQxK_4_T.css} | 2 +- public/build/assets/HomeView-CCZOZ97d.js | 1 + ...ew-CkcxjjBz.js => LibraryView-CCh9WW_b.js} | 2 +- ...w-CURB-Bg6.js => SettingsView-BeFRLItq.js} | 2 +- ...iew-CsBgOdoF.js => UploadView-CG5RzNO2.js} | 2 +- .../{index-aP_uBWCi.js => index-Dt4UyE7n.js} | 4 +- public/build/index.html | 2 +- 12 files changed, 59 insertions(+), 148 deletions(-) rename public/build/assets/{BaseBottomSheet-CXcLrct9.js => BaseBottomSheet-DYbXyS7e.js} (98%) rename public/build/assets/{DevicePicker-MIsIaweQ.js => DevicePicker-kY4qiZhO.js} (96%) delete mode 100644 public/build/assets/HomeView-B-yz7GFB.js rename public/build/assets/{HomeView-hc6H9m3_.css => HomeView-BQxK_4_T.css} (68%) create mode 100644 public/build/assets/HomeView-CCZOZ97d.js rename public/build/assets/{LibraryView-CkcxjjBz.js => LibraryView-CCh9WW_b.js} (98%) rename public/build/assets/{SettingsView-CURB-Bg6.js => SettingsView-BeFRLItq.js} (92%) rename public/build/assets/{UploadView-CsBgOdoF.js => UploadView-CG5RzNO2.js} (98%) rename public/build/assets/{index-aP_uBWCi.js => index-Dt4UyE7n.js} (99%) diff --git a/frontend/src/test/views/HomeView.test.ts b/frontend/src/test/views/HomeView.test.ts index 7033f29..1c63572 100644 --- a/frontend/src/test/views/HomeView.test.ts +++ b/frontend/src/test/views/HomeView.test.ts @@ -106,8 +106,8 @@ describe('HomeView', () => { }) } - // HV-01: N devices renders a carousel of N large FrameCard stubs + N dots - it('renders one FrameCard per device in a carousel when multiple devices present', async () => { + // HV-01: N devices renders a vertical stack of N large FrameCard stubs + it('renders one FrameCard per device in a vertical stack when multiple devices present', async () => { const devicesStore = useDevicesStore() devicesStore.devices = [ makeDevice({ id: 1, name: 'Frame A' }), @@ -119,58 +119,27 @@ describe('HomeView', () => { const wrapper = mountView() await wrapper.vm.$nextTick() - expect(wrapper.find('.home-view__carousel').exists()).toBe(true) + expect(wrapper.find('.home-view__stack').exists()).toBe(true) + expect(wrapper.findAll('.home-view__slide')).toHaveLength(3) expect(wrapper.findAll('.frame-card-stub')).toHaveLength(3) - // All cards should be the large variant (no more compact stack) + // All cards should be the large variant (no compact / no carousel) const cards = wrapper.findAllComponents({ name: 'FrameCard' }) for (const c of cards) expect(c.props('size')).toBe('large') - // One navigation dot per device - expect(wrapper.findAll('.home-view__dot')).toHaveLength(3) }) - it('marks the first dot active by default and updates active dot on scroll', async () => { + it('labels each slide with the device name for accessibility', async () => { const devicesStore = useDevicesStore() devicesStore.devices = [ - makeDevice({ id: 1, name: 'A' }), - makeDevice({ id: 2, name: 'B' }), + makeDevice({ id: 1, name: 'Living Room' }), + makeDevice({ id: 2, name: 'Bedroom' }), ] vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() const wrapper = mountView() await wrapper.vm.$nextTick() - let dots = wrapper.findAll('.home-view__dot') - expect(dots[0].classes()).toContain('home-view__dot--active') - expect(dots[1].classes()).not.toContain('home-view__dot--active') - - // Simulate the carousel having scrolled to the second slide - const carousel = wrapper.find('.home-view__carousel').element as HTMLElement - Object.defineProperty(carousel, 'clientWidth', { configurable: true, value: 360 }) - Object.defineProperty(carousel, 'scrollLeft', { configurable: true, value: 360 }) - await wrapper.find('.home-view__carousel').trigger('scroll') - await wrapper.vm.$nextTick() - - dots = wrapper.findAll('.home-view__dot') - expect(dots[1].classes()).toContain('home-view__dot--active') - expect(dots[0].classes()).not.toContain('home-view__dot--active') - }) - - it('clicking a dot scrolls the carousel to that slide', async () => { - const devicesStore = useDevicesStore() - devicesStore.devices = [makeDevice({ id: 1 }), makeDevice({ id: 2 }), makeDevice({ id: 3 })] - vi.spyOn(devicesStore, 'fetchDevices').mockResolvedValue() - - const wrapper = mountView() - await wrapper.vm.$nextTick() - - const carousel = wrapper.find('.home-view__carousel').element as HTMLElement - Object.defineProperty(carousel, 'clientWidth', { configurable: true, value: 360 }) - const scrollToSpy = vi.fn() - ;(carousel as any).scrollTo = scrollToSpy - - await wrapper.findAll('.home-view__dot')[2].trigger('click') - await wrapper.vm.$nextTick() - - expect(scrollToSpy).toHaveBeenCalledWith(expect.objectContaining({ left: 720, behavior: 'smooth' })) + const slides = wrapper.findAll('.home-view__slide') + expect(slides[0].attributes('aria-label')).toBe('Living Room') + expect(slides[1].attributes('aria-label')).toBe('Bedroom') }) // HV-01b: single device still renders one FrameCard (large variant branch) diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index b8c4f98..44a0c81 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -37,51 +37,34 @@ /> - - + @@ -131,7 +114,7 @@ +