email box empty styling
This commit is contained in:
parent
1ee8a58117
commit
5cb49f4cb1
19 changed files with 547 additions and 67 deletions
|
|
@ -5,6 +5,7 @@ import {
|
|||
ChevronDown,
|
||||
ChevronRight,
|
||||
ClipboardList,
|
||||
CircleHelp,
|
||||
FileSearch,
|
||||
FileText,
|
||||
ListChecks,
|
||||
|
|
@ -12,9 +13,11 @@ import {
|
|||
Paperclip,
|
||||
PenLine,
|
||||
Send,
|
||||
Settings,
|
||||
Sparkles,
|
||||
} from "lucide-react";
|
||||
import AgentInsight from "./AgentInsight";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
import SenderAvatar from "./SenderAvatar";
|
||||
import { inboxContext } from "../utils/inboxSignals";
|
||||
|
||||
|
|
@ -57,7 +60,7 @@ const cards = [
|
|||
function greeting(name) {
|
||||
const hour = new Date().getHours();
|
||||
const period = hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening";
|
||||
return `${period}, ${name}!`;
|
||||
return `${period}, ${name}`;
|
||||
}
|
||||
|
||||
function statCaption(card, stats) {
|
||||
|
|
@ -79,6 +82,9 @@ export default function AgAgentPanel({
|
|||
focus,
|
||||
insight,
|
||||
loading = false,
|
||||
mailboxEmpty = false,
|
||||
isBrandNewMailbox = false,
|
||||
onCompose,
|
||||
onQuickAction,
|
||||
onOpenEmail,
|
||||
onAgentAsk,
|
||||
|
|
@ -89,6 +95,9 @@ export default function AgAgentPanel({
|
|||
const firstName = account?.first_name || "there";
|
||||
const allMetricsZero = metricTotal(stats) === 0;
|
||||
const hasQuery = agentQuery.trim().length > 0;
|
||||
const showOnboarding = !insight && !loading && mailboxEmpty && isBrandNewMailbox;
|
||||
const showQuietEmpty = !insight && !loading && mailboxEmpty && !isBrandNewMailbox;
|
||||
const showFullPanel = !insight && !loading && !mailboxEmpty;
|
||||
|
||||
function submitAgentQuery(event) {
|
||||
event.preventDefault();
|
||||
|
|
@ -126,7 +135,13 @@ export default function AgAgentPanel({
|
|||
{!insight && (
|
||||
<div className="agent-panel-hello">
|
||||
<strong>{greeting(firstName)}</strong>
|
||||
<span>Here's what needs your attention today.</span>
|
||||
<span>
|
||||
{showOnboarding
|
||||
? "Your new inbox is ready."
|
||||
: showQuietEmpty
|
||||
? "Nothing needs your attention right now."
|
||||
: "Here's what needs your attention today."}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -137,16 +152,28 @@ export default function AgAgentPanel({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!insight && !loading && allMetricsZero && (
|
||||
{showOnboarding && (
|
||||
<div className="agent-ready-card">
|
||||
<p className="agent-ready-lead">
|
||||
<Check size={14} strokeWidth={2} /> AGMail is ready
|
||||
</p>
|
||||
<p>
|
||||
No messages yet. I'll start organizing and surfacing important
|
||||
items as email arrives.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showQuietEmpty && (
|
||||
<div className="agent-caught-up">
|
||||
<p className="agent-caught-up-lead">
|
||||
<Check size={14} strokeWidth={2} /> You're all caught up.
|
||||
</p>
|
||||
<p>No urgent messages or outstanding actions.</p>
|
||||
<p>Your inbox is clear for now.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!insight && !loading && !allMetricsZero && (
|
||||
{showFullPanel && !allMetricsZero && (
|
||||
<div className="agent-stat-grid">
|
||||
{cards.map((card) => {
|
||||
const Icon = card.icon;
|
||||
|
|
@ -173,42 +200,91 @@ export default function AgAgentPanel({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!insight && focus && !allMetricsZero && <p className="agent-panel-note">{focus}</p>}
|
||||
{showFullPanel && allMetricsZero && (
|
||||
<div className="agent-caught-up">
|
||||
<p className="agent-caught-up-lead">
|
||||
<Check size={14} strokeWidth={2} /> You're all caught up.
|
||||
</p>
|
||||
<p>No urgent messages or outstanding actions.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!insight && focus && showFullPanel && !allMetricsZero && (
|
||||
<p className="agent-panel-note">{focus}</p>
|
||||
)}
|
||||
|
||||
<section className="agent-quick">
|
||||
<h3>Quick actions</h3>
|
||||
<h3>{showOnboarding ? "Quick start" : "Quick actions"}</h3>
|
||||
<div className="agent-quick-grid">
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("summarize")}>
|
||||
<Sparkles {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Summarize inbox</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("draft")}>
|
||||
<PenLine {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Draft replies</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("attachments")}>
|
||||
<FileSearch {...ICON_SM} className="agent-quick-icon agent-quick-icon-muted" />
|
||||
<span>Find attachments</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("actions")}>
|
||||
<ListChecks {...ICON_SM} className="agent-quick-icon agent-quick-icon-slate" />
|
||||
<span>Show action items</span>
|
||||
</button>
|
||||
{showOnboarding ? (
|
||||
<>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("learn")}>
|
||||
<AgentSpark className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Meet AG Agent</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onCompose?.()}>
|
||||
<PenLine {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Compose email</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("preferences")}>
|
||||
<Settings {...ICON_SM} className="agent-quick-icon agent-quick-icon-muted" />
|
||||
<span>Set preferences</span>
|
||||
</button>
|
||||
<button type="button" className="agent-quick-btn" onClick={() => onQuickAction("learn")}>
|
||||
<CircleHelp {...ICON_SM} className="agent-quick-icon agent-quick-icon-slate" />
|
||||
<span>Learn AGMail</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-quick-btn"
|
||||
onClick={() => onQuickAction("summarize")}
|
||||
disabled={mailboxEmpty}
|
||||
>
|
||||
<Sparkles {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Summarize inbox</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-quick-btn"
|
||||
onClick={() => onQuickAction("draft")}
|
||||
disabled={mailboxEmpty}
|
||||
>
|
||||
<PenLine {...ICON_SM} className="agent-quick-icon agent-quick-icon-purple" />
|
||||
<span>Draft replies</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-quick-btn"
|
||||
onClick={() => onQuickAction("attachments")}
|
||||
disabled={mailboxEmpty}
|
||||
>
|
||||
<FileSearch {...ICON_SM} className="agent-quick-icon agent-quick-icon-muted" />
|
||||
<span>Find attachments</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="agent-quick-btn"
|
||||
onClick={() => onQuickAction("actions")}
|
||||
disabled={mailboxEmpty}
|
||||
>
|
||||
<ListChecks {...ICON_SM} className="agent-quick-icon agent-quick-icon-slate" />
|
||||
<span>Show action items</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{!insight && (
|
||||
{showFullPanel && !insight && (
|
||||
<section className="agent-context-block agent-agenda">
|
||||
<div className="agent-context-head">
|
||||
<h3>Today's agenda</h3>
|
||||
<button type="button" className="agent-context-link">View calendar</button>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="agent-loading-rows" aria-hidden="true">
|
||||
<span className="agent-loading-pulse" />
|
||||
<span className="agent-loading-pulse short" />
|
||||
</div>
|
||||
) : today.length > 0 ? (
|
||||
{today.length > 0 ? (
|
||||
<ul className="agent-agenda-list">
|
||||
{today.map((item) => (
|
||||
<li key={`${item.time}-${item.label}`} className="agent-agenda-row">
|
||||
|
|
@ -228,14 +304,10 @@ export default function AgAgentPanel({
|
|||
</section>
|
||||
)}
|
||||
|
||||
{!insight && (
|
||||
{showFullPanel && !insight && (
|
||||
<section className="agent-context-block agent-attention-block">
|
||||
<h3>Needs attention</h3>
|
||||
{loading ? (
|
||||
<div className="agent-loading-rows" aria-hidden="true">
|
||||
<span className="agent-loading-pulse" />
|
||||
</div>
|
||||
) : needsReply.length > 0 ? (
|
||||
{needsReply.length > 0 ? (
|
||||
<ul className="agent-attention-list">
|
||||
{needsReply.slice(0, 3).map((item) => (
|
||||
<li key={item.id}>
|
||||
|
|
@ -259,7 +331,7 @@ export default function AgAgentPanel({
|
|||
</section>
|
||||
)}
|
||||
|
||||
{!insight && deadlines.length > 0 && (
|
||||
{showFullPanel && !insight && deadlines.length > 0 && (
|
||||
<section className="agent-context-block agent-deadlines">
|
||||
<h3>Deadlines</h3>
|
||||
<ul>
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ export default function DashboardPage() {
|
|||
agentNav={agentNav}
|
||||
onLogout={logout}
|
||||
onOpenSettings={() => selectMailItem("Settings")}
|
||||
onCompose={() => setComposeOpen(true)}
|
||||
onStats={handleStats}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -428,6 +429,7 @@ export default function DashboardPage() {
|
|||
account={account}
|
||||
onLogout={logout}
|
||||
onOpenSettings={() => selectMailItem("Settings")}
|
||||
onCompose={() => setComposeOpen(true)}
|
||||
/>
|
||||
)}
|
||||
{!companyActive && active === "Trash" && (
|
||||
|
|
@ -438,6 +440,7 @@ export default function DashboardPage() {
|
|||
account={account}
|
||||
onLogout={logout}
|
||||
onOpenSettings={() => selectMailItem("Settings")}
|
||||
onCompose={() => setComposeOpen(true)}
|
||||
/>
|
||||
)}
|
||||
{companyActive === "Overview" && isDomainOwner && !isOrgSetupComplete(account.id) && (
|
||||
|
|
|
|||
18
frontend/src/components/MailboxEmptyIllustration.jsx
Normal file
18
frontend/src/components/MailboxEmptyIllustration.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Mail } from "lucide-react";
|
||||
import AgentSpark from "./AgentSpark";
|
||||
|
||||
export default function MailboxEmptyIllustration() {
|
||||
return (
|
||||
<div className="mailbox-empty-illustration" aria-hidden="true">
|
||||
<span className="mailbox-empty-spark mailbox-empty-spark-top">
|
||||
<AgentSpark />
|
||||
</span>
|
||||
<span className="mailbox-empty-icon-wrap">
|
||||
<Mail size={22} strokeWidth={1.75} />
|
||||
</span>
|
||||
<span className="mailbox-empty-spark mailbox-empty-spark-bottom">
|
||||
<AgentSpark />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
frontend/src/components/MailboxEmptyState.jsx
Normal file
63
frontend/src/components/MailboxEmptyState.jsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { Check } from "lucide-react";
|
||||
import MailboxEmptyIllustration from "./MailboxEmptyIllustration";
|
||||
import { getMailboxEmptyCopy } from "../utils/mailboxEmptyCopy";
|
||||
|
||||
export default function MailboxEmptyState({
|
||||
mailbox,
|
||||
title,
|
||||
category,
|
||||
isBrandNew,
|
||||
hasFilters,
|
||||
hasSearch,
|
||||
lens,
|
||||
onCompose,
|
||||
onRefresh,
|
||||
}) {
|
||||
const copy = getMailboxEmptyCopy({
|
||||
mailbox,
|
||||
title,
|
||||
category,
|
||||
isBrandNew,
|
||||
hasFilters,
|
||||
hasSearch,
|
||||
lens,
|
||||
});
|
||||
|
||||
function handlePrimary() {
|
||||
if (copy.primaryCta?.action === "compose") onCompose?.();
|
||||
}
|
||||
|
||||
function handleSecondary() {
|
||||
if (copy.secondaryCta?.action === "refresh") onRefresh?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mailbox-empty-body">
|
||||
<div className="mailbox-empty-content">
|
||||
<MailboxEmptyIllustration />
|
||||
<h3>{copy.heading}</h3>
|
||||
<p>{copy.description}</p>
|
||||
{(copy.primaryCta || copy.secondaryCta) && (
|
||||
<div className="mailbox-empty-actions">
|
||||
{copy.primaryCta && (
|
||||
<button type="button" className="mailbox-empty-cta" onClick={handlePrimary}>
|
||||
{copy.primaryCta.label}
|
||||
</button>
|
||||
)}
|
||||
{copy.secondaryCta && (
|
||||
<button type="button" className="mailbox-empty-link" onClick={handleSecondary}>
|
||||
{copy.secondaryCta.label}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{copy.showActiveBadge && (
|
||||
<p className="mailbox-empty-badge">
|
||||
<Check size={14} strokeWidth={2} />
|
||||
Email address is active
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import {
|
|||
ChevronRight,
|
||||
CircleHelp,
|
||||
Filter,
|
||||
Inbox,
|
||||
LayoutGrid,
|
||||
List,
|
||||
Mail,
|
||||
|
|
@ -31,8 +30,10 @@ import AgAgentPanel from "./AgAgentPanel";
|
|||
import AgentSpark from "./AgentSpark";
|
||||
import CommandPalette from "./CommandPalette";
|
||||
import { useAgentInsight } from "../hooks/useAgentInsight";
|
||||
import { useMailboxMaturity } from "../hooks/useMailboxMaturity";
|
||||
import MailDetail from "./MailDetail";
|
||||
import MailRow from "./MailRow";
|
||||
import MailboxEmptyState from "./MailboxEmptyState";
|
||||
import MailboxSelectionBar from "./MailboxSelectionBar";
|
||||
import StickyNotesPanel from "./StickyNotesPanel";
|
||||
import {
|
||||
|
|
@ -70,6 +71,7 @@ export default function MailboxView({
|
|||
agentNav = null,
|
||||
onLogout,
|
||||
onOpenSettings,
|
||||
onCompose,
|
||||
onStats,
|
||||
}) {
|
||||
const token = sessionStorage.getItem("agmail_access_token");
|
||||
|
|
@ -165,6 +167,11 @@ export default function MailboxView({
|
|||
[checked],
|
||||
);
|
||||
const selectedCount = selectedIds.length;
|
||||
const hasFilters = filters.read !== "all" || Boolean(filters.sender.trim())
|
||||
|| Boolean(filters.dateFrom) || Boolean(filters.dateTo);
|
||||
const hasSearch = Boolean(filters.query.trim());
|
||||
const { isBrandNew } = useMailboxMaturity(account, emails, status.loading);
|
||||
const isEmpty = !status.loading && !status.error && listedEmails.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (!onStats) return;
|
||||
|
|
@ -495,8 +502,6 @@ export default function MailboxView({
|
|||
setDetail(null);
|
||||
}
|
||||
|
||||
const hasFilters =
|
||||
filters.read !== "all" || filters.sender || filters.dateFrom || filters.dateTo;
|
||||
const initials = `${account?.first_name?.[0] || ""}${account?.last_name?.[0] || ""}`.toUpperCase();
|
||||
const rangeStart = listedEmails.length ? cursorHistory.length * PAGE_SIZE + 1 : 0;
|
||||
const rangeEnd = cursorHistory.length * PAGE_SIZE + listedEmails.length;
|
||||
|
|
@ -623,19 +628,25 @@ export default function MailboxView({
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div className="mailbox-rows" aria-busy={status.loading}>
|
||||
{status.loading && <div className="mailbox-state">Loading messages…</div>}
|
||||
<div className={`mailbox-rows${isEmpty ? " is-empty" : ""}`} aria-busy={status.loading}>
|
||||
{status.loading && <div className="mailbox-state loading">Loading messages…</div>}
|
||||
{status.error && (
|
||||
<div className="mailbox-state error" role="alert">
|
||||
<p>{status.error}</p><button type="button" onClick={load}>Try again</button>
|
||||
</div>
|
||||
)}
|
||||
{!status.loading && !status.error && listedEmails.length === 0 && (
|
||||
<div className="mailbox-state empty">
|
||||
<span><Inbox size={22} /></span>
|
||||
<strong>No messages here</strong>
|
||||
<p>{hasFilters || filters.query || lens !== "all" ? "Try another category, label, or search." : `Your ${title.toLowerCase()} is empty.`}</p>
|
||||
</div>
|
||||
{isEmpty && (
|
||||
<MailboxEmptyState
|
||||
mailbox={mailbox}
|
||||
title={title}
|
||||
category={mailbox === "inbox" ? category : null}
|
||||
isBrandNew={isBrandNew}
|
||||
hasFilters={hasFilters}
|
||||
hasSearch={hasSearch}
|
||||
lens={lens}
|
||||
onCompose={onCompose}
|
||||
onRefresh={load}
|
||||
/>
|
||||
)}
|
||||
{!status.loading && !status.error && listedEmails.map((email) => (
|
||||
<MailRow
|
||||
|
|
@ -655,19 +666,24 @@ export default function MailboxView({
|
|||
))}
|
||||
</div>
|
||||
|
||||
{!status.loading && listedEmails.length > 0 && (
|
||||
{!status.loading && (
|
||||
<footer className="mailbox-pagination">
|
||||
<span className="mailbox-pagination-range">
|
||||
{`${rangeStart}–${rangeEnd}`}
|
||||
{nextCursor ? "" : ` · ${rangeEnd} loaded`}
|
||||
{listedEmails.length === 0
|
||||
? "0 messages"
|
||||
: `${rangeStart}–${rangeEnd}${nextCursor ? "" : ` · ${rangeEnd} loaded`}`}
|
||||
</span>
|
||||
<div className="mailbox-pagination-controls">
|
||||
<button type="button" title="Previous page" aria-label="Previous page" disabled={!cursorHistory.length} onClick={previousPage}>
|
||||
<ChevronLeft size={16} strokeWidth={1.75} />
|
||||
</button>
|
||||
<button type="button" title="Next page" aria-label="Next page" disabled={!nextCursor} onClick={nextPage}>
|
||||
<ChevronRight size={16} strokeWidth={1.75} />
|
||||
</button>
|
||||
{listedEmails.length > 0 && (
|
||||
<>
|
||||
<button type="button" title="Previous page" aria-label="Previous page" disabled={!cursorHistory.length} onClick={previousPage}>
|
||||
<ChevronLeft size={16} strokeWidth={1.75} />
|
||||
</button>
|
||||
<button type="button" title="Next page" aria-label="Next page" disabled={!nextCursor} onClick={nextPage}>
|
||||
<ChevronRight size={16} strokeWidth={1.75} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="mailbox-pagination-density" aria-label="List density">
|
||||
<button type="button" className="active" title="Comfortable density" aria-label="Comfortable density"><List size={15} strokeWidth={1.75} /></button>
|
||||
|
|
@ -702,6 +718,9 @@ export default function MailboxView({
|
|||
focus={agentFocus}
|
||||
insight={insight}
|
||||
loading={status.loading}
|
||||
mailboxEmpty={isEmpty}
|
||||
isBrandNewMailbox={isBrandNew && mailbox === "inbox"}
|
||||
onCompose={onCompose}
|
||||
onQuickAction={runQuickAction}
|
||||
onOpenEmail={(emailId) => {
|
||||
const email = listedEmails.find((item) => item.id === emailId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue