From 9b5a1ae7527278cc0793e51f5238b90b38ad07d7 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 16:16:09 -0800 Subject: [PATCH] =?UTF-8?q?feat(avocet):=20EmailCardStack=20=E2=80=94=20sw?= =?UTF-8?q?ipe=20gestures,=20depth=20shadows,=20dismissal=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/EmailCardStack.test.ts | 59 +++++++++ web/src/components/EmailCardStack.vue | 138 ++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 web/src/components/EmailCardStack.test.ts create mode 100644 web/src/components/EmailCardStack.vue diff --git a/web/src/components/EmailCardStack.test.ts b/web/src/components/EmailCardStack.test.ts new file mode 100644 index 0000000..6bb0aaa --- /dev/null +++ b/web/src/components/EmailCardStack.test.ts @@ -0,0 +1,59 @@ +import { mount } from '@vue/test-utils' +import EmailCardStack from './EmailCardStack.vue' +import { describe, it, expect } from 'vitest' + +const item = { + id: 'abc', + subject: 'Interview at Acme', + body: 'We would like to schedule...', + from: 'hr@acme.com', + date: '2026-03-01', + source: 'imap:test', +} + +describe('EmailCardStack', () => { + it('renders the email subject', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + expect(w.text()).toContain('Interview at Acme') + }) + + it('renders shadow cards for depth effect', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + expect(w.findAll('.card-shadow')).toHaveLength(2) + }) + + it('applies dismiss-label class when dismissType is label', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: 'label' } }) + expect(w.find('.card-wrapper').classes()).toContain('dismiss-label') + }) + + it('applies dismiss-discard class when dismissType is discard', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: 'discard' } }) + expect(w.find('.card-wrapper').classes()).toContain('dismiss-discard') + }) + + it('applies dismiss-skip class when dismissType is skip', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: 'skip' } }) + expect(w.find('.card-wrapper').classes()).toContain('dismiss-skip') + }) + + it('no dismiss class when dismissType is null', () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false, dismissType: null } }) + const wrapperClasses = w.find('.card-wrapper').classes() + expect(wrapperClasses).not.toContain('dismiss-label') + expect(wrapperClasses).not.toContain('dismiss-discard') + expect(wrapperClasses).not.toContain('dismiss-skip') + }) + + it('emits drag-start on dragstart event', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + await w.find('.card-stack').trigger('dragstart') + expect(w.emitted('drag-start')).toBeTruthy() + }) + + it('emits drag-end on dragend event', async () => { + const w = mount(EmailCardStack, { props: { item, isBucketMode: false } }) + await w.find('.card-stack').trigger('dragend') + expect(w.emitted('drag-end')).toBeTruthy() + }) +}) diff --git a/web/src/components/EmailCardStack.vue b/web/src/components/EmailCardStack.vue new file mode 100644 index 0000000..078d325 --- /dev/null +++ b/web/src/components/EmailCardStack.vue @@ -0,0 +1,138 @@ + + + + +