Rework website EmailsVisual to match twenty-front emails tab (#22146)

- Rework `EmailsVisual` to match twenty-front's emails tab: bordered
thread card with dividers, `Inbox` header sizing, and twenty-front's
row/sender/date colors and hard-clipped sender names (kept `+ Compose`).
- Replace the "not shared" row with four fully-populated shared threads
(subjects/bodies sourced from twenty-server's dev seed), two
participants each, full `MMM D, YYYY` dates.

<img width="575" height="607" alt="image"
src="https://github.com/user-attachments/assets/68d1fd6d-e030-4f3c-b775-687f7e1ab173"
/>
This commit is contained in:
Abdullah.
2026-06-25 15:02:44 +05:00
committed by GitHub
parent cdd1aba3e0
commit 491b9e954a
6 changed files with 157 additions and 70 deletions
@@ -0,0 +1,48 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { render, screen } from '@testing-library/react';
import { EmailsVisual } from './EmailsVisual';
i18n.load('en', {});
i18n.activate('en');
function renderVisual() {
return render(
<I18nProvider i18n={i18n}>
<EmailsVisual active />
</I18nProvider>,
);
}
describe('EmailsVisual', () => {
it('renders the inbox chrome', () => {
renderVisual();
expect(screen.getByText('Compose')).toBeInTheDocument();
expect(
screen.getByText('Inbox', { exact: false, selector: 'span' }),
).toBeInTheDocument();
});
it('renders every thread subject', () => {
renderVisual();
expect(screen.getByText('Partnerships - Q4 Strategy')).toBeInTheDocument();
expect(screen.getByText('Proposal Submission')).toBeInTheDocument();
expect(screen.getByText('Follow-up on Discussion')).toBeInTheDocument();
expect(screen.getByText('Customer Feedback')).toBeInTheDocument();
});
it('renders thread previews, participants and dates', () => {
renderVisual();
expect(
screen.getByText(
"Hey team, I've been in touch with Notion and Figma about potential integrations.",
),
).toBeInTheDocument();
expect(screen.getByText('Félix, Thomas')).toBeInTheDocument();
expect(screen.getByText('Jun 24, 2026')).toBeInTheDocument();
});
});
@@ -1,5 +1,7 @@
'use client';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { IconPlus } from '@tabler/icons-react';
import { THEME_LIGHT } from 'twenty-ui/theme';
@@ -41,14 +43,13 @@ const InboxTitle = styled.span`
align-items: baseline;
color: ${THEME_LIGHT.font.color.primary};
display: flex;
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
font-size: ${previewFontSize(THEME_LIGHT.font.size.lg)};
font-weight: ${THEME_LIGHT.font.weight.semiBold};
gap: 6px;
gap: ${THEME_LIGHT.spacing(2)};
`;
const InboxCount = styled.span`
color: ${THEME_LIGHT.font.color.light};
font-weight: ${THEME_LIGHT.font.weight.regular};
`;
const ComposeButton = styled.span`
@@ -63,34 +64,38 @@ const ComposeButton = styled.span`
padding: 4px 8px;
`;
const ThreadList = styled.div`
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
const ThreadCard = styled.div`
border: 1px solid ${THEME_LIGHT.border.color.medium};
border-radius: ${THEME_LIGHT.border.radius.sm};
margin: 0 16px;
overflow: hidden;
& > * + * {
border-top: 1px solid ${THEME_LIGHT.border.color.light};
}
`;
export function EmailsVisual({ active: _active }: { active: boolean }) {
const { i18n } = useLingui();
return (
<Root>
<RecordTabHeader active="Emails" />
<Panel>
<InboxHeader>
<InboxTitle>
Inbox
{i18n._(msg`Inbox`)}
<InboxCount>{THREADS.length}</InboxCount>
</InboxTitle>
<ComposeButton>
<IconPlus size={12} stroke={2} />
Compose
{i18n._(msg`Compose`)}
</ComposeButton>
</InboxHeader>
<ThreadList>
<ThreadCard>
{THREADS.map((thread) => (
<ThreadRow key={thread.date} thread={thread} />
))}
</ThreadList>
</ThreadCard>
</Panel>
</Root>
);
@@ -0,0 +1,36 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { render, screen } from '@testing-library/react';
import { type EmailThread } from '../types/email-thread';
import { ThreadRow } from './ThreadRow';
i18n.load('en', {});
i18n.activate('en');
const THREAD: EmailThread = {
date: 'Jan 1, 2026',
messageCount: 3,
participants: [
{ avatarUrl: '', name: 'Ada' },
{ avatarUrl: '', name: 'Linus' },
],
preview: { id: 'A short preview.' },
subject: { id: 'A subject line' },
};
describe('ThreadRow', () => {
it('renders the subject, preview, participants, count and date', () => {
render(
<I18nProvider i18n={i18n}>
<ThreadRow thread={THREAD} />
</I18nProvider>,
);
expect(screen.getByText('A subject line')).toBeInTheDocument();
expect(screen.getByText('A short preview.')).toBeInTheDocument();
expect(screen.getByText('Ada, Linus')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('Jan 1, 2026')).toBeInTheDocument();
});
});
@@ -1,33 +1,36 @@
'use client';
import { useLingui } from '@lingui/react';
import { styled } from '@linaria/react';
import { IconLock } from '@tabler/icons-react';
import { THEME_LIGHT } from 'twenty-ui/theme';
import { PersonAvatar } from '@/app-preview/primitives/PersonAvatar';
import { previewFontSize } from '@/app-preview/preview-font-size';
import { PersonAvatar } from '@/app-preview/primitives/PersonAvatar';
import { type EmailThread } from '../types/email-thread';
const Row = styled.div`
align-items: center;
background-color: ${THEME_LIGHT.background.secondary};
box-sizing: border-box;
color: ${THEME_LIGHT.font.color.primary};
color: ${THEME_LIGHT.font.color.secondary};
display: flex;
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
gap: 8px;
height: 48px;
padding: 0 16px;
&:hover {
background-color: ${THEME_LIGHT.background.transparent.light};
background-color: transparent;
}
`;
const Heading = styled.div`
const Sender = styled.div`
align-items: center;
display: flex;
max-width: 40%;
flex: 0 0 20%;
min-width: 0;
overflow: hidden;
width: fit-content;
`;
const AvatarStack = styled.div`
@@ -41,18 +44,15 @@ const AvatarStack = styled.div`
`;
const SenderNames = styled.span`
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
margin: 0 4px;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const ThreadCount = styled.span`
const MessageCount = styled.span`
color: ${THEME_LIGHT.font.color.tertiary};
flex-shrink: 0;
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
`;
const SubjectBody = styled.div`
@@ -66,9 +66,6 @@ const SubjectBody = styled.div`
const Subject = styled.span`
color: ${THEME_LIGHT.font.color.primary};
flex-shrink: 0;
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -77,29 +74,13 @@ const Subject = styled.span`
const Preview = styled.span`
color: ${THEME_LIGHT.font.color.tertiary};
flex: 1;
font-size: ${previewFontSize(THEME_LIGHT.font.size.md)};
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const NotShared = styled.span`
align-items: center;
background: ${THEME_LIGHT.background.transparent.lighter};
border: 1px solid ${THEME_LIGHT.border.color.light};
border-radius: ${THEME_LIGHT.border.radius.sm};
color: ${THEME_LIGHT.font.color.tertiary};
display: inline-flex;
flex: 1;
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
gap: 4px;
height: 20px;
padding: 0 4px;
`;
const ReceivedAt = styled.span`
color: ${THEME_LIGHT.font.color.tertiary};
flex-shrink: 0;
font-size: ${previewFontSize(THEME_LIGHT.font.size.sm)};
padding: 0 4px;
@@ -107,9 +88,10 @@ const ReceivedAt = styled.span`
`;
export function ThreadRow({ thread }: { thread: EmailThread }) {
const { i18n } = useLingui();
return (
<Row>
<Heading>
<Sender>
<AvatarStack>
{thread.participants.map((participant) => (
<PersonAvatar
@@ -127,20 +109,11 @@ export function ThreadRow({ thread }: { thread: EmailThread }) {
.map((participant) => participant.name)
.join(', ')}
</SenderNames>
<ThreadCount>{thread.messageCount}</ThreadCount>
</Heading>
<MessageCount>{thread.messageCount}</MessageCount>
</Sender>
<SubjectBody>
{thread.shared ? (
<>
<Subject>{thread.subject}</Subject>
<Preview>{thread.preview}</Preview>
</>
) : (
<NotShared>
<IconLock size={14} stroke={2} />
Not shared
</NotShared>
)}
<Subject>{i18n._(thread.subject)}</Subject>
<Preview>{i18n._(thread.preview)}</Preview>
</SubjectBody>
<ReceivedAt>{thread.date}</ReceivedAt>
</Row>
@@ -1,3 +1,5 @@
import { msg } from '@lingui/core/macro';
import { sharedAssetUrls } from '@/app-preview/data/shared-asset-urls';
import { type EmailThread } from '../types/email-thread';
@@ -6,21 +8,43 @@ const PEOPLE = sharedAssetUrls.peopleAvatars;
export const THREADS: EmailThread[] = [
{
date: '1:30pm',
messageCount: 2,
participants: [{ avatarUrl: PEOPLE.anonymousMike, name: 'Mike' }],
shared: false,
},
{
date: '4 nov 2023',
date: 'Jun 24, 2026',
messageCount: 4,
participants: [
{ avatarUrl: PEOPLE.anonymousFelix, name: 'Félix' },
{ avatarUrl: PEOPLE.anonymousThomas, name: 'Thomas' },
],
preview:
"Hey team, I've been in touch with Notion and Figma about potential integrations.",
shared: true,
subject: 'Partnerships - Q4 Strategy',
preview: msg`Hey team, I've been in touch with Notion and Figma about potential integrations.`,
subject: msg`Partnerships - Q4 Strategy`,
},
{
date: 'Jun 23, 2026',
messageCount: 2,
participants: [
{ avatarUrl: PEOPLE.anonymousLaura, name: 'Laura' },
{ avatarUrl: PEOPLE.anonymousIndira, name: 'Indira' },
],
preview: msg`Dear Team, I am pleased to submit our proposal for your consideration. We have carefully reviewed your requirements.`,
subject: msg`Proposal Submission`,
},
{
date: 'Jun 20, 2026',
messageCount: 3,
participants: [
{ avatarUrl: PEOPLE.anonymousIndira, name: 'Indira' },
{ avatarUrl: PEOPLE.anonymousMike, name: 'Mike' },
],
preview: msg`Hi, I wanted to follow up on our conversation from last week regarding the new initiative.`,
subject: msg`Follow-up on Discussion`,
},
{
date: 'Jun 18, 2026',
messageCount: 1,
participants: [
{ avatarUrl: PEOPLE.anonymousThomas, name: 'Thomas' },
{ avatarUrl: PEOPLE.anonymousLaura, name: 'Laura' },
],
preview: msg`Hello, I wanted to share some positive feedback from our recent customer satisfaction survey.`,
subject: msg`Customer Feedback`,
},
];
@@ -1,3 +1,5 @@
import { type MessageDescriptor } from '@lingui/core';
type Participant = {
avatarUrl: string;
name: string;
@@ -7,7 +9,6 @@ export type EmailThread = {
date: string;
messageCount: number;
participants: Participant[];
preview?: string;
shared: boolean;
subject?: string;
preview: MessageDescriptor;
subject: MessageDescriptor;
};