feat(home): replace horizontal carousel with vertical scroll-snap stack
CI / test (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 18:45:02 -04:00
parent da0396788f
commit 396d4e941f
12 changed files with 59 additions and 148 deletions
+11 -42
View File
@@ -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)