feat(avocet): EmailCard component — subject, from/date, body preview, expand/collapse

This commit is contained in:
pyr0ball 2026-03-03 16:03:01 -08:00
parent e823c84196
commit e05ac885d7
2 changed files with 152 additions and 0 deletions

View file

@ -0,0 +1,39 @@
import { mount } from '@vue/test-utils'
import EmailCard from './EmailCard.vue'
import { describe, it, expect } from 'vitest'
const item = {
id: 'abc', subject: 'Interview Invitation',
body: 'Hi there, we would like to schedule a phone screen with you. This will be a 30-minute call.',
from: 'recruiter@acme.com', date: '2026-03-01', source: 'imap:test',
}
describe('EmailCard', () => {
it('renders subject', () => {
const w = mount(EmailCard, { props: { item } })
expect(w.text()).toContain('Interview Invitation')
})
it('renders from and date', () => {
const w = mount(EmailCard, { props: { item } })
expect(w.text()).toContain('recruiter@acme.com')
expect(w.text()).toContain('2026-03-01')
})
it('renders truncated body by default', () => {
const w = mount(EmailCard, { props: { item } })
expect(w.text()).toContain('Hi there')
})
it('emits expand on button click', async () => {
const w = mount(EmailCard, { props: { item } })
await w.find('[data-testid="expand-btn"]').trigger('click')
expect(w.emitted('expand')).toBeTruthy()
})
it('shows collapse button when expanded', () => {
const w = mount(EmailCard, { props: { item, expanded: true } })
expect(w.find('[data-testid="collapse-btn"]').exists()).toBe(true)
expect(w.find('[data-testid="expand-btn"]').exists()).toBe(false)
})
})

View file

@ -0,0 +1,113 @@
<template>
<article class="email-card" :class="{ expanded }">
<header class="card-header">
<h2 class="subject">{{ item.subject }}</h2>
<div class="meta">
<span class="from" :title="item.from">{{ item.from }}</span>
<time class="date" :datetime="item.date">{{ item.date }}</time>
</div>
</header>
<div class="card-body">
<p class="body-text">{{ displayBody }}</p>
</div>
<footer class="card-footer">
<button
v-if="!expanded"
data-testid="expand-btn"
class="expand-btn"
aria-label="Expand full email"
@click="$emit('expand')"
>
Show more
</button>
<button
v-else
data-testid="collapse-btn"
class="expand-btn"
aria-label="Collapse email"
@click="$emit('collapse')"
>
Show less
</button>
</footer>
</article>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { QueueItem } from '../stores/label'
const props = defineProps<{ item: QueueItem; expanded?: boolean }>()
defineEmits<{ expand: []; collapse: [] }>()
const PREVIEW_LINES = 6
const displayBody = computed(() => {
if (props.expanded) return props.item.body
const lines = props.item.body.split('\n').slice(0, PREVIEW_LINES).join('\n')
return lines.length < props.item.body.length ? lines + '…' : lines
})
</script>
<style scoped>
.email-card {
background: var(--color-surface-raised);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
padding: var(--space-6);
font-family: var(--font-body);
box-shadow: var(--shadow-md);
display: flex;
flex-direction: column;
gap: var(--space-4);
}
.subject {
font-size: 1.1rem;
font-weight: 600;
color: var(--color-text);
margin: 0;
line-height: 1.4;
}
.meta {
display: flex;
gap: var(--space-4);
font-size: 0.875rem;
color: var(--color-text-muted);
margin-top: var(--space-2);
flex-wrap: wrap;
}
.body-text {
color: var(--color-text);
font-size: 0.9375rem;
line-height: 1.6;
white-space: pre-wrap;
margin: 0;
}
.card-footer {
display: flex;
justify-content: flex-end;
}
.expand-btn {
background: none;
border: none;
color: var(--app-primary);
cursor: pointer;
font-size: 0.875rem;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
transition: opacity var(--transition);
}
.expand-btn:hover { opacity: 0.75; }
.expand-btn:focus-visible {
outline: 2px solid var(--app-primary);
outline-offset: 3px;
}
</style>