Style the email-inbox message list with a list group (#2996)

This commit is contained in:
Paweł Kuna authored and GitHub committed 2026-09-08 00:54:48 +02:00
1 parent b8e61dbea7
commit d9ed9428de
5 files changed
+96 -100

No files matched your search

-12
View File
@@ -1,7 +1,6 @@
# Classes used in the rendered html that no stylesheet defines on purpose: # Classes used in the rendered html that no stylesheet defines on purpose:
# JavaScript hooks and demo-only markers. One per line; delete a line when the hook goes. # JavaScript hooks and demo-only markers. One per line; delete a line when the hook goes.
callout-note callout-note
checkbox-wrapper-mail
datagrid-content datagrid-content
datagrid-item datagrid-item
datatable datatable
@@ -11,17 +10,7 @@ docs-search
docs-theme-toggle docs-theme-toggle
dropzone-msg-desc dropzone-msg-desc
dropzone-msg-title dropzone-msg-title
email-action-icons
email-action-icons-item
email-border
email-content
email-date
email-list
email-sender-info
email-subject
email-title
fallback fallback
file-offcanvas
github-dark github-dark
icon-tabler icon-tabler
icon-tabler-pencil icon-tabler-pencil
@@ -47,7 +36,6 @@ sort-score
sort-status sort-status
sort-tags sort-tags
sort-type sort-type
star-toggle
table-tbody table-tbody
tblr-illustrations-computer-fix-a tblr-illustrations-computer-fix-a
tblr-illustrations-computer-fix-b tblr-illustrations-computer-fix-b
+5
View File
@@ -0,0 +1,5 @@
---
"@tabler/preview": patch
---
Fixed the unstyled message list on the `email-inbox` page: a hoverable list group with label dots, dates and row actions.
+77 -87
View File
@@ -10,27 +10,33 @@ import Progress from '@ui/Progress.astro'
import Modal from '@shared/components/modals/Modal.astro' import Modal from '@shared/components/modals/Modal.astro'
import CaptureModal from '@shared/components/CaptureModal.astro' import CaptureModal from '@shared/components/CaptureModal.astro'
import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro' import NewEmailModalContent from '@shared/components/modals/NewEmailModalContent.astro'
import ListGroup from '@ui/ListGroup.astro'
import ListGroupItem from '@ui/ListGroupItem.astro'
import { formatShortDate } from '@shared/lib/date-format'
import mails from '@data/mails.json' import mails from '@data/mails.json'
// Label colors shared by the folder sidebar and the dots in the message list.
const labelColors: Record<string, string> = { Updates: 'info', Friends: 'warning', Family: 'success', Social: 'primary', Important: 'danger', Promotions: 'purple' }
--- ---
<DefaultLayout title="Email inbox" pageHeader="Inbox" pageMenu="extra.email-inbox" pageLibs={['hugerte']}> <DefaultLayout title="Email inbox" pageHeader="Inbox" pageMenu="extra.email-inbox" pageLibs={['hugerte']}>
<Card> <Card>
<div class="row g-0"> <div class="row g-0">
<div class="col-xxl-3 email-border border-end"> <div class="col-xxl-3 border-end">
<Offcanvas responsive="xxl" direction="start" class="h-100 file-offcanvas" tabindex="-1" id="emailSidebaroffcanvas"> <Offcanvas responsive="xxl" direction="start" class="h-100" tabindex="-1" id="emailSidebaroffcanvas">
<div class="card-body h-100"> <div class="card-body h-100">
<div> <div>
<Button icon="pencil" text="Compose" color="primary" class="d-none d-sm-block" modalId="new-email" /> <Button icon="pencil" text="Compose" color="primary" block class="d-none d-sm-block" modalId="new-email" />
</div> </div>
<div class="mt-3 nav nav-vertical"> <div class="mt-3 nav nav-vertical nav-pills">
<a href="#" class="nav-link text-danger fw-bold"> <a href="#" class="nav-link active">
<Icon name="inbox" class="me-2" /> <Icon name="inbox" class="me-2" />
Inbox<span class="badge bg-danger ms-auto">{mails.length}</span> Inbox<span class="badge bg-primary-lt ms-auto">{mails.length}</span>
</a> </a>
<a href="#" class="nav-link"><Icon name="star" class="me-2" />Starred</a> <a href="#" class="nav-link"><Icon name="star" class="me-2" />Starred</a>
<a href="#" class="nav-link"><Icon name="clock" class="me-2" />Snoozed</a> <a href="#" class="nav-link"><Icon name="clock" class="me-2" />Snoozed</a>
<a href="#" class="nav-link"><Icon name="file" class="me-2" />Draft<span class="badge bg-info ms-auto">32</span></a> <a href="#" class="nav-link"><Icon name="file" class="me-2" />Draft<span class="badge bg-info-lt ms-auto">32</span></a>
<a href="#" class="nav-link"><Icon name="mail-up" class="me-2" />Sent Mail</a> <a href="#" class="nav-link"><Icon name="mail-up" class="me-2" />Sent Mail</a>
<a href="#" class="nav-link"><Icon name="trash" class="me-2" />Trash</a> <a href="#" class="nav-link"><Icon name="trash" class="me-2" />Trash</a>
<a href="#" class="nav-link"><Icon name="tag" class="me-2" />Important</a> <a href="#" class="nav-link"><Icon name="tag" class="me-2" />Important</a>
@@ -39,25 +45,14 @@ import mails from '@data/mails.json'
<div class="mt-4"> <div class="mt-4">
<Subheader as="h6">Labels</Subheader> <Subheader as="h6">Labels</Subheader>
<div class="mt-2 nav nav-vertical"> <div class="mt-2 nav nav-vertical nav-pills">
<a href="#" class="nav-link"> {
<div class="badge bg-info me-2"></div> Updates Object.entries(labelColors).map(([label, color]) => (
</a> <a href="#" class="nav-link">
<a href="#" class="nav-link"> <span class={`badge bg-${color} me-2`} /> {label}
<div class="badge bg-warning me-2"></div> Friends </a>
</a> ))
<a href="#" class="nav-link"> }
<div class="badge bg-success me-2"></div> Family
</a>
<a href="#" class="nav-link">
<div class="badge bg-primary me-2"></div> Social
</a>
<a href="#" class="nav-link">
<div class="badge bg-danger me-2"></div> Important
</a>
<a href="#" class="nav-link">
<div class="badge bg-purple me-2"></div> Promotions
</a>
</div> </div>
</div> </div>
@@ -112,12 +107,11 @@ import mails from '@data/mails.json'
</div> </div>
</ButtonGroup> </ButtonGroup>
<ButtonGroup> <ButtonGroup class="ms-auto">
<button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <button type="button" class="btn dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<Icon name="dots" /> More <Icon name="dots" /> More
</button> </button>
<div class="dropdown-menu"> <div class="dropdown-menu dropdown-menu-end">
<span class="dropdown-header">More Options :</span>
<a class="dropdown-item" href="#">Mark as Unread</a> <a class="dropdown-item" href="#">Mark as Unread</a>
<a class="dropdown-item" href="#">Add to Tasks</a> <a class="dropdown-item" href="#">Add to Tasks</a>
<a class="dropdown-item" href="#">Add Star</a> <a class="dropdown-item" href="#">Add Star</a>
@@ -125,70 +119,66 @@ import mails from '@data/mails.json'
</div> </div>
</ButtonGroup> </ButtonGroup>
</div> </div>
</div>
<div class="mt-3"> <div class="card-body p-0">
<ul class="email-list"> <ListGroup flush hoverable>
{ {
mails && mails.length > 0 ? ( mails.length > 0 ? (
mails.map((mail) => ( mails.map((mail) => (
<li> <ListGroupItem>
<div class="email-sender-info"> <div class="row align-items-center g-2">
<div class="checkbox-wrapper-mail"> <div class="col-auto">
<div class="form-check"> <input type="checkbox" class="form-check-input m-0" aria-label={`Select "${mail.subject}"`} />
<input type="checkbox" class="form-check-input" id={`mail-${mail.id}`} /> </div>
<label class="form-check-label" for={`mail-${mail.id}`} /> <div class="col-auto">
</div> <a href="#" class:list={['list-group-item-actions', mail.starred && 'show']} aria-label={mail.starred ? 'Remove star' : 'Add star'}>
</div> <Icon name="star" color={mail.starred ? 'yellow' : 'muted'} />
<span class="star-toggle"> </a>
<Icon name="star" /> </div>
</span> <div class="col-3 col-lg-2 text-truncate">
<a href="#" class="email-title"> <a href="#" class="text-body">
{mail.sender} {mail.sender}
</a> </a>
</div> </div>
<div class="col text-truncate">
<div class="email-content"> <a href="#" class="text-body">
<a href="#" class="email-subject"> {mail.subject}
{mail.subject} &nbsp;–&nbsp;
<span>{mail.preview}</span>
</a> </a>
<div class="email-date">{mail.date}</div> <span class="text-secondary"> – {mail.preview}</span>
</div> </div>
<div class="col-auto d-none d-lg-block">
<div class="email-action-icons"> {mail.labels.map((label) => (
<ul class="list-inline"> <span class={`badge bg-${labelColors[label] ?? 'secondary'}`} title={label} aria-label={label} />
<li class="list-inline-item"> ))}
<a href="#" aria-label="Archive">
<Icon name="archive" />
</a>
</li>
<li class="list-inline-item">
<a href="#" aria-label="Delete">
<Icon name="trash" />
</a>
</li>
<li class="list-inline-item">
<a href="#" aria-label="Mark as read">
<Icon name="mail-opened" class="email-action-icons-item" />
</a>
</li>
<li class="list-inline-item">
<a href="#" aria-label="Snooze">
<Icon name="clock" />
</a>
</li>
</ul>
</div> </div>
</li> <div class="col-auto d-none d-md-block">
)) <div class="list-group-item-actions d-flex gap-2">
) : ( <a href="#" class="text-secondary" aria-label="Archive">
<li class="text-muted">No emails</li> <Icon name="archive" />
) </a>
} <a href="#" class="text-secondary" aria-label="Delete">
</ul> <Icon name="trash" />
</div> </a>
<a href="#" class="text-secondary" aria-label="Mark as read">
<div class="row"> <Icon name="mail-opened" />
</a>
<a href="#" class="text-secondary" aria-label="Snooze">
<Icon name="clock" />
</a>
</div>
</div>
<div class="col-auto text-secondary text-end">{formatShortDate(new Date(mail.date))}</div>
</div>
</ListGroupItem>
))
) : (
<ListGroupItem class="text-secondary">No emails</ListGroupItem>
)
}
</ListGroup>
</div>
<div class="card-body">
<div class="row align-items-center">
<div class="col-7 mt-1"> <div class="col-7 mt-1">
Showing 1 - {mails.length} of {mails.length} Showing 1 - {mails.length} of {mails.length}
</div> </div>
+8 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { formatCommitDate, formatLongDate, formatUtcTimestamp, toUnixSeconds } from './date-format' import { formatCommitDate, formatShortDate, formatLongDate, formatUtcTimestamp, toUnixSeconds } from './date-format'
describe('formatCommitDate', () => { describe('formatCommitDate', () => {
it('reformats a full timestamp to "D Mon YYYY"', () => { it('reformats a full timestamp to "D Mon YYYY"', () => {
@@ -36,3 +36,10 @@ describe('formatUtcTimestamp', () => {
expect(formatUtcTimestamp(date)).toBe('2025-11-28 08:05 +0000') expect(formatUtcTimestamp(date)).toBe('2025-11-28 08:05 +0000')
}) })
}) })
describe('formatShortDate', () => {
it('formats the UTC day and short month', () => {
expect(formatShortDate(new Date('2025-08-27T11:49:00Z'))).toBe('27 Aug')
expect(formatShortDate(new Date('2025-01-01T00:30:00Z'))).toBe('1 Jan')
})
})
+6
View File
@@ -1,6 +1,7 @@
// Demo data date formatting quirks: // Demo data date formatting quirks:
// - unix seconds are shifted by local timezone offset (not the true epoch) // - unix seconds are shifted by local timezone offset (not the true epoch)
// - long-date format uses UTC wall-clock components // - long-date format uses UTC wall-clock components
const SHORT_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
/** Unix seconds shifted by the local timezone offset (not the true epoch). */ /** Unix seconds shifted by the local timezone offset (not the true epoch). */
@@ -13,6 +14,11 @@ export function formatLongDate(date: Date): string {
return `${MONTHS[date.getUTCMonth()]} ${String(date.getUTCDate()).padStart(2, '0')}, ${date.getUTCFullYear()}` return `${MONTHS[date.getUTCMonth()]} ${String(date.getUTCDate()).padStart(2, '0')}, ${date.getUTCFullYear()}`
} }
/** Formats a date as "D Mon" using its UTC components, e.g. "27 Aug". */
export function formatShortDate(date: Date): string {
return `${date.getUTCDate()} ${SHORT_MONTHS[date.getUTCMonth()]}`
}
/** Reformats a commit-style timestamp ("Thu Nov 28 08:48:33 2025 +0100") to "28 Nov 2025". */ /** Reformats a commit-style timestamp ("Thu Nov 28 08:48:33 2025 +0100") to "28 Nov 2025". */
export function formatCommitDate(date: string): string { export function formatCommitDate(date: string): string {
const m = date.match(/^\w+ (\w+) (\d+) [\d:]+ (\d+)/) const m = date.match(/^\w+ (\w+) (\d+) [\d:]+ (\d+)/)