ui improvements

This commit is contained in:
debu-2 2026-10-03 21:46:03 +05:30
parent cce9a8c9ba
commit 78aac5a925
7 changed files with 769 additions and 138 deletions

View file

@ -0,0 +1,110 @@
import {
Calendar,
FileSearch,
ListChecks,
PenLine,
Receipt,
Sparkles,
Star,
} from "lucide-react";
import { inboxIntelligence } from "../utils/inboxSignals";
function greeting(name) {
const hour = new Date().getHours();
const period = hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening";
return `${period}, ${name}`;
}
const cards = [
{
key: "important",
label: "Important emails",
hint: "Need a look",
icon: Star,
tone: "rose",
},
{
key: "actionItems",
label: "Action items",
hint: "Need your response",
icon: ListChecks,
tone: "blue",
},
{
key: "meetings",
label: "Meetings mentioned",
hint: "In this view",
icon: Calendar,
tone: "green",
},
{
key: "invoices",
label: "Invoices & bills",
hint: "This week",
icon: Receipt,
tone: "amber",
},
];
export default function AgAgentPanel({ account, emails, focus, onQuickAction }) {
const stats = inboxIntelligence(emails);
const firstName = account?.first_name || "there";
return (
<section className="agent-panel" aria-label="AG Agent">
<header className="agent-panel-head">
<span className="agent-panel-mark" aria-hidden="true"><Sparkles size={15} /></span>
<div>
<h2>AG Agent</h2>
<p>Your email co-pilot</p>
</div>
</header>
<div className="agent-panel-hello">
<strong>{greeting(firstName)}</strong>
<span>Here&apos;s what needs your attention.</span>
{focus && <p>{focus}</p>}
</div>
<div className="agent-stat-grid">
{cards.map((card) => {
const Icon = card.icon;
const count = stats[card.key] || 0;
return (
<button
key={card.key}
type="button"
className={`agent-stat agent-stat-${card.tone}`}
onClick={() => onQuickAction(card.key)}
>
<Icon size={15} />
<strong>{count}</strong>
<span>{card.label}</span>
<small>{card.key === "important" && stats.importantFromClients
? `${stats.importantFromClients} from clients`
: card.hint}</small>
</button>
);
})}
</div>
<div className="agent-quick">
<h3>Quick actions</h3>
<div>
<button type="button" onClick={() => onQuickAction("summarize")}>
<Sparkles size={14} /> Summarize inbox
</button>
<button type="button" onClick={() => onQuickAction("draft")}>
<PenLine size={14} /> Draft replies
</button>
<button type="button" onClick={() => onQuickAction("attachments")}>
<FileSearch size={14} /> Find attachments
</button>
<button type="button" onClick={() => onQuickAction("actions")}>
<ListChecks size={14} /> Show action items
</button>
</div>
</div>
</section>
);
}

View file

@ -1,6 +1,8 @@
import {
Bot,
ChevronDown,
Clock3,
FileEdit,
Inbox,
ListFilter,
LogOut,
@ -11,11 +13,12 @@ import {
Settings,
ShieldAlert,
Sparkles,
Star,
Tags,
Trash2,
X,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { getCurrentAccount, getMyCompanyDomain } from "../api";
import Brand from "./Brand";
import CompanyDnsSetup from "./CompanyDnsSetup";
@ -23,12 +26,22 @@ import ComposeModal from "./ComposeModal";
import MailboxView from "./MailboxView";
const mailItems = [
[Inbox, "Inbox"],
[Send, "Outbox"],
[Inbox, "Inbox", "unread"],
[Star, "Starred", "starred"],
[Clock3, "Snoozed"],
[Send, "Sent"],
[FileEdit, "Drafts"],
[ShieldAlert, "Spam"],
[Trash2, "Trash"],
[Clock3, "Scheduled"],
[Settings, "Settings"],
];
const labels = [
["Important", "rose"],
["Projects", "violet"],
["Clients", "blue"],
["Finance", "amber"],
["Personal", "green"],
];
const agentItems = [
@ -36,6 +49,7 @@ const agentItems = [
[ListFilter, "Filters"],
[Sparkles, "Summary"],
[Plug, "Integration"],
[Settings, "Settings"],
];
function clearSession() {
@ -43,20 +57,21 @@ function clearSession() {
sessionStorage.removeItem("agmail_account");
}
function NavGroup({ icon: Icon, title, items, active, onSelect }) {
function NavGroup({ icon: Icon, title, items, active, counts, onSelect }) {
return (
<section className="dashboard-nav-group">
<h2><Icon size={15} />{title}</h2>
<h2><Icon size={13} />{title}</h2>
<nav aria-label={title}>
{items.map(([ItemIcon, label]) => (
{items.map(([ItemIcon, label, countKey]) => (
<button
className={active === label ? "active" : ""}
key={label}
onClick={() => onSelect(label)}
type="button"
>
<ItemIcon size={17} />
<ItemIcon size={16} />
<span>{label}</span>
{countKey && counts?.[countKey] > 0 && <em>{counts[countKey]}</em>}
</button>
))}
</nav>
@ -75,6 +90,10 @@ export default function DashboardPage() {
const [notice, setNotice] = useState("");
const [loading, setLoading] = useState(true);
const [mailRefreshKey, setMailRefreshKey] = useState(0);
const [labelsOpen, setLabelsOpen] = useState(true);
const [lens, setLens] = useState("all");
const [mailStats, setMailStats] = useState({ unread: 0, starred: 0, important: 0 });
const handleStats = useCallback((stats) => setMailStats(stats), []);
useEffect(() => {
const token = sessionStorage.getItem("agmail_access_token");
@ -110,7 +129,19 @@ export default function DashboardPage() {
}
function selectItem(item) {
setActive(item);
if (item === "Starred") {
setActive("Inbox");
setLens("starred");
} else if (item === "Sent") {
setActive("Outbox");
setLens("all");
} else if (labels.some(([name]) => name === item)) {
setActive("Inbox");
setLens(item);
} else {
setActive(item);
setLens("all");
}
setMenuOpen(false);
}
@ -179,7 +210,30 @@ export default function DashboardPage() {
<button className="dashboard-compose" type="button" onClick={() => setComposeOpen(true)}>
<Mail size={17} />Compose
</button>
<NavGroup icon={Mail} title="Mail" items={mailItems} active={active} onSelect={selectItem} />
<NavGroup icon={Mail} title="Mail" items={mailItems} active={lens === "starred" ? "Starred" : active === "Outbox" ? "Sent" : active} counts={mailStats} onSelect={selectItem} />
<section className="dashboard-labels">
<button type="button" className="dashboard-labels-toggle" onClick={() => setLabelsOpen((open) => !open)} aria-expanded={labelsOpen}>
<Tags size={16} />
<span>Labels</span>
<ChevronDown size={14} className={labelsOpen ? "open" : ""} />
</button>
{labelsOpen && (
<nav aria-label="Labels">
{labels.map(([name, tone]) => (
<button
className={lens === name ? "active" : ""}
key={name}
type="button"
onClick={() => selectItem(name)}
>
<i className={`label-dot label-dot-${tone}`} />
<span>{name}</span>
{name === "Important" && mailStats.important > 0 && <em>{mailStats.important}</em>}
</button>
))}
</nav>
)}
</section>
<NavGroup icon={Bot} title="Agent" items={agentItems} active={active} onSelect={selectItem} />
<div className="dashboard-profile">
<span>{account.first_name[0]}{account.last_name[0]}</span>
@ -195,9 +249,38 @@ export default function DashboardPage() {
<span className="dashboard-account-pill">{workspaceLabel}</span>
</div>
)}
{active === "Inbox" && <MailboxView mailbox="inbox" title="Inbox" refreshKey={mailRefreshKey} />}
{active === "Outbox" && <MailboxView mailbox="sent" title="Sent" refreshKey={mailRefreshKey} />}
{active === "Trash" && <MailboxView mailbox="trash" title="Trash" refreshKey={mailRefreshKey} />}
{active === "Inbox" && (
<MailboxView
mailbox="inbox"
title="Inbox"
refreshKey={mailRefreshKey}
account={account}
lens={lens}
onLogout={logout}
onOpenSettings={() => selectItem("Settings")}
onStats={handleStats}
/>
)}
{active === "Outbox" && (
<MailboxView
mailbox="sent"
title="Sent"
refreshKey={mailRefreshKey}
account={account}
onLogout={logout}
onOpenSettings={() => selectItem("Settings")}
/>
)}
{active === "Trash" && (
<MailboxView
mailbox="trash"
title="Trash"
refreshKey={mailRefreshKey}
account={account}
onLogout={logout}
onOpenSettings={() => selectItem("Settings")}
/>
)}
{!["Inbox", "Outbox", "Trash"].includes(active) && (
<section className="dashboard-empty">
<span><Sparkles size={24} /></span>

View file

@ -1,6 +1,15 @@
import { MailOpen, Star } from "lucide-react";
import {
Archive,
Clock3,
MailOpen,
Paperclip,
Sparkles,
Star,
Trash2,
} from "lucide-react";
import SenderAvatar from "./SenderAvatar";
import { senderDisplayName } from "../utils/senderAvatar";
import { attachmentCount, emailLabels } from "../utils/inboxSignals";
function formatTimestamp(value) {
if (!value) return "";
@ -21,7 +30,19 @@ function formatTimestamp(value) {
}).format(date);
}
export default function MailRow({ email, selected, onOpen, onToggleStar }) {
export default function MailRow({
email,
selected,
checked,
onOpen,
onToggleStar,
onToggleCheck,
onTrash,
onMarkUnread,
onArchive,
onSnooze,
onSummarize,
}) {
const unread = email.is_read === false;
const starred = email.is_starred === true;
const subject = email.subject?.trim() || "(No subject)";
@ -29,6 +50,8 @@ export default function MailRow({ email, selected, onOpen, onToggleStar }) {
const timestamp =
email.received_at || email.sent_at || email.created_at || email.updated_at;
const senderName = senderDisplayName(email);
const labels = emailLabels(email);
const files = attachmentCount(email);
function handleKeyDown(event) {
if (event.key === "Enter" || event.key === " ") {
@ -37,9 +60,9 @@ export default function MailRow({ email, selected, onOpen, onToggleStar }) {
}
}
function handleStarClick(event) {
function stop(event, action) {
event.stopPropagation();
onToggleStar?.(email, !starred);
action?.();
}
return (
@ -51,25 +74,48 @@ export default function MailRow({ email, selected, onOpen, onToggleStar }) {
onKeyDown={handleKeyDown}
aria-label={`${unread ? "Unread" : "Read"} email from ${senderName}: ${subject}`}
>
<span className="mailbox-read-dot" aria-hidden="true">
{unread ? <i /> : <MailOpen size={14} />}
</span>
<SenderAvatar email={email} className="mailbox-avatar" />
<span className="mailbox-sender">{senderName}</span>
<span className="mailbox-summary">
<strong>{subject}</strong>
{snippet && <span><b aria-hidden="true">—</b> {snippet}</span>}
</span>
<time dateTime={timestamp || undefined}>{formatTimestamp(timestamp)}</time>
<input
className="mailbox-check"
type="checkbox"
checked={checked}
aria-label={`Select message from ${senderName}`}
onClick={(event) => event.stopPropagation()}
onChange={() => onToggleCheck?.(email)}
/>
<button
type="button"
className={`mailbox-star${starred ? " active" : ""}`}
onClick={handleStarClick}
onClick={(event) => stop(event, () => onToggleStar?.(email, !starred))}
aria-label={starred ? "Unstar message" : "Star message"}
aria-pressed={starred}
>
<Star size={15} fill={starred ? "currentColor" : "none"} />
</button>
<SenderAvatar email={email} className="mailbox-avatar" />
<span className="mailbox-sender">
{unread && <i className="mailbox-unread-pip" aria-hidden="true" />}
{senderName}
</span>
<span className="mailbox-summary">
<strong>{subject}</strong>
{snippet && <span>{snippet}</span>}
{labels.map((label) => (
<em key={label.name} className={`mail-label mail-label-${label.tone}`}>{label.name}</em>
))}
{files > 0 && (
<em className="mail-attachment" title={`${files} attachment${files === 1 ? "" : "s"}`}>
<Paperclip size={12} /> {files > 1 ? files : ""}
</em>
)}
</span>
<time dateTime={timestamp || undefined}>{formatTimestamp(timestamp)}</time>
<div className="mailbox-hover-actions">
<button type="button" title="Archive" aria-label="Archive" onClick={(event) => stop(event, () => onArchive?.(email))}><Archive size={14} /></button>
<button type="button" title="Snooze" aria-label="Snooze" onClick={(event) => stop(event, () => onSnooze?.(email))}><Clock3 size={14} /></button>
<button type="button" title="Mark unread" aria-label="Mark unread" onClick={(event) => stop(event, () => onMarkUnread?.(email))}><MailOpen size={14} /></button>
<button type="button" title="Delete" aria-label="Delete" onClick={(event) => stop(event, () => onTrash?.(email))}><Trash2 size={14} /></button>
<button type="button" title="Summarize" aria-label="Summarize" onClick={(event) => stop(event, () => onSummarize?.(email))}><Sparkles size={14} /></button>
</div>
</div>
);
}

View file

@ -1,12 +1,17 @@
import {
Bell,
ChevronDown,
ChevronLeft,
ChevronRight,
CircleHelp,
Filter,
Inbox,
RefreshCw,
Search,
Settings,
Sparkles,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import {
getEmail,
listEmails,
@ -16,11 +21,19 @@ import {
setEmailStar,
trashEmail,
} from "../api";
import AgAgentPanel from "./AgAgentPanel";
import MailDetail from "./MailDetail";
import MailRow from "./MailRow";
import StickyNotesPanel from "./StickyNotesPanel";
import {
categoryCounts,
emailCategory,
inboxIntelligence,
matchesLens,
} from "../utils/inboxSignals";
const PAGE_SIZE = 25;
const CATEGORIES = ["Primary", "Updates", "Promotions", "Social", "Finance", "All Mail"];
function resultPage(result) {
if (Array.isArray(result)) return { items: result, nextCursor: null };
@ -30,17 +43,30 @@ function resultPage(result) {
};
}
export default function MailboxView({ mailbox, title, refreshKey = 0 }) {
export default function MailboxView({
mailbox,
title,
refreshKey = 0,
account,
lens = "all",
onLogout,
onOpenSettings,
onStats,
}) {
const token = sessionStorage.getItem("agmail_access_token");
const [emails, setEmails] = useState([]);
const [nextCursor, setNextCursor] = useState(null);
const [cursor, setCursor] = useState(null);
const [cursorHistory, setCursorHistory] = useState([]);
const [selected, setSelected] = useState(null);
const [checked, setChecked] = useState({});
const [hidden, setHidden] = useState({});
const [detail, setDetail] = useState(null);
const [status, setStatus] = useState({ loading: true, error: "" });
const [detailStatus, setDetailStatus] = useState({ loading: false, error: "", busy: false });
const [filtersOpen, setFiltersOpen] = useState(false);
const [category, setCategory] = useState("Primary");
const [agentFocus, setAgentFocus] = useState("");
const [filters, setFilters] = useState({
query: "",
read: "all",
@ -79,6 +105,39 @@ export default function MailboxView({ mailbox, title, refreshKey = 0 }) {
return () => window.clearTimeout(timeout);
}, [load, refreshKey]);
useEffect(() => {
function focusSearch(event) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
document.getElementById("workspace-search")?.focus();
}
}
window.addEventListener("keydown", focusSearch);
return () => window.removeEventListener("keydown", focusSearch);
}, []);
const visibleEmails = useMemo(
() => emails.filter((email) => !hidden[email.id] && matchesLens(email, lens)),
[emails, hidden, lens],
);
const listedEmails = useMemo(() => {
if (mailbox !== "inbox" || category === "All Mail") return visibleEmails;
return visibleEmails.filter((email) => emailCategory(email) === category);
}, [category, mailbox, visibleEmails]);
const counts = useMemo(() => categoryCounts(visibleEmails), [visibleEmails]);
useEffect(() => {
if (!onStats) return;
const intel = inboxIntelligence(emails);
onStats({
unread: emails.filter((email) => email.is_read === false).length,
starred: emails.filter((email) => email.is_starred).length,
important: intel.important,
});
}, [emails, onStats]);
function changeFilter(name, value) {
setFilters((current) => ({ ...current, [name]: value }));
setCursor(null);
@ -121,7 +180,7 @@ export default function MailboxView({ mailbox, title, refreshKey = 0 }) {
}
try {
await setEmailStar(email.id, isStarred, token);
} catch (error) {
} catch {
setEmails((current) =>
current.map((item) => item.id === email.id ? { ...item, is_starred: !isStarred } : item),
);
@ -131,6 +190,79 @@ export default function MailboxView({ mailbox, title, refreshKey = 0 }) {
}
}
async function markUnread(email) {
setEmails((current) =>
current.map((item) => item.id === email.id ? { ...item, is_read: false } : item),
);
try {
await setEmailRead(email.id, false, token);
} catch {
setEmails((current) =>
current.map((item) => item.id === email.id ? { ...item, is_read: email.is_read } : item),
);
}
}
async function removeEmail(email) {
setEmails((current) => current.filter((item) => item.id !== email.id));
if (detail?.id === email.id) {
setSelected(null);
setDetail(null);
}
try {
await trashEmail(email.id, token);
} catch (error) {
setAgentFocus(error.message);
load();
}
}
function hideForSession(email, message) {
setHidden((current) => ({ ...current, [email.id]: true }));
if (detail?.id === email.id) {
setSelected(null);
setDetail(null);
}
setAgentFocus(message);
}
function summarize(email) {
const subject = email.subject?.trim() || "(No subject)";
const snippet = email.snippet || email.preview || email.body_text || "No preview available.";
setAgentFocus(`${subject} — ${snippet}`);
}
function runQuickAction(action) {
const intel = inboxIntelligence(listedEmails);
if (action === "summarize") {
setAgentFocus(
listedEmails.length
? `${listedEmails.length} messages in ${category}. ${intel.actionItems} look like they need a reply, ${intel.meetings} mention a meeting, and ${intel.invoices} look like bills.`
: "Nothing in this view to summarize yet.",
);
return;
}
if (action === "draft") {
const first = listedEmails.find((email) => email.is_read === false) || listedEmails[0];
setAgentFocus(first
? `Draft a reply to ${first.subject || "the latest message"} when you open compose.`
: "No message is waiting for a reply.");
return;
}
if (action === "attachments" || action === "actions" || action === "important" || action === "actionItems" || action === "meetings" || action === "invoices") {
const labels = {
attachments: "attachments",
actions: "action items",
actionItems: "action items",
important: "important mail",
meetings: "meetings",
invoices: "invoices",
};
setCategory(action === "invoices" ? "Finance" : action === "important" ? "Primary" : category);
setAgentFocus(`Showing signals for ${labels[action] || action} from the messages already loaded.`);
}
}
async function toggleRead(isRead) {
if (!detail) return;
setDetailStatus((current) => ({ ...current, busy: true, error: "" }));
@ -185,120 +317,168 @@ export default function MailboxView({ mailbox, title, refreshKey = 0 }) {
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;
return (
<section className="mailbox-workspace">
<div className="mailbox-main-column">
<div className="dashboard-content-heading">
<div><p>Personal workspace</p><h1>{title}</h1></div>
<span className="dashboard-account-pill">Individual</span>
</div>
<div className={`mailbox-shell${selected ? " detail-visible" : ""}`}>
<div className="mailbox-list-pane">
<div className="mailbox-toolbar">
<label>
<Search size={16} />
<input
type="search"
value={filters.query}
onChange={(event) => changeFilter("query", event.target.value)}
placeholder={`Search ${title.toLowerCase()}`}
/>
</label>
<button
className={filtersOpen || hasFilters ? "active" : ""}
type="button"
onClick={() => setFiltersOpen((open) => !open)}
aria-expanded={filtersOpen}
>
<Filter size={16} /> <span>Filters</span>
</button>
<button type="button" onClick={load} aria-label="Refresh messages" title="Refresh">
<RefreshCw size={16} />
</button>
</div>
{filtersOpen && (
<div className="mailbox-filters">
<label>Read status
<select value={filters.read} onChange={(event) => changeFilter("read", event.target.value)}>
<option value="all">All messages</option>
<option value="unread">Unread</option>
<option value="read">Read</option>
</select>
</label>
<label>Sender
<input value={filters.sender} onChange={(event) => changeFilter("sender", event.target.value)} placeholder="name@example.com" />
</label>
<label>From
<input type="date" value={filters.dateFrom} onChange={(event) => changeFilter("dateFrom", event.target.value)} />
</label>
<label>To
<input type="date" value={filters.dateTo} onChange={(event) => changeFilter("dateTo", event.target.value)} />
</label>
<header className="workspace-header">
<label className="workspace-search">
<Search size={16} />
<input
id="workspace-search"
type="search"
value={filters.query}
onChange={(event) => changeFilter("query", event.target.value)}
placeholder="Search emails, people, or ask your agent..."
/>
<kbd>Ctrl K</kbd>
</label>
<button
className={`workspace-filter${filtersOpen || hasFilters ? " active" : ""}`}
type="button"
onClick={() => setFiltersOpen((open) => !open)}
aria-expanded={filtersOpen}
aria-label="Advanced filters"
>
<Filter size={16} />
</button>
<button className="workspace-ask" type="button" onClick={() => runQuickAction("summarize")}>
<Sparkles size={15} /> Ask Agent
</button>
<button type="button" className="workspace-icon" aria-label="Refresh" onClick={load}><RefreshCw size={16} /></button>
<button type="button" className="workspace-icon" aria-label="Notifications"><Bell size={16} /></button>
<button type="button" className="workspace-icon" aria-label="Help"><CircleHelp size={16} /></button>
<button type="button" className="workspace-icon" aria-label="Settings" onClick={onOpenSettings}><Settings size={16} /></button>
<button type="button" className="workspace-account" onClick={onLogout} title="Log out">
<span>{initials || "AG"}</span>
<div>
<strong>{account?.first_name} {account?.last_name}</strong>
<small>{account?.email}</small>
</div>
)}
<ChevronDown size={14} />
</button>
</header>
<div className="mailbox-rows" aria-busy={status.loading}>
{status.loading && <div className="mailbox-state">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 className="mailbox-stage">
<div className="mailbox-main-column">
<div className={`mailbox-shell${selected ? " detail-visible" : ""}`}>
<div className="mailbox-list-pane">
{mailbox === "inbox" && (
<div className="mailbox-categories" role="tablist" aria-label="Inbox categories">
{CATEGORIES.map((name) => (
<button
key={name}
type="button"
role="tab"
aria-selected={category === name}
className={category === name ? "active" : ""}
onClick={() => setCategory(name)}
>
{name}
{name !== "All Mail" && counts[name] > 0 && <span>{counts[name]}</span>}
</button>
))}
<small className="mailbox-range">
{listedEmails.length ? `${rangeStart}–${rangeEnd}` : title}
</small>
</div>
)}
{mailbox !== "inbox" && (
<div className="mailbox-categories">
<button type="button" className="active">{title}</button>
</div>
)}
{filtersOpen && (
<div className="mailbox-filters">
<label>Read status
<select value={filters.read} onChange={(event) => changeFilter("read", event.target.value)}>
<option value="all">All messages</option>
<option value="unread">Unread</option>
<option value="read">Read</option>
</select>
</label>
<label>Sender
<input value={filters.sender} onChange={(event) => changeFilter("sender", event.target.value)} placeholder="name@example.com" />
</label>
<label>From
<input type="date" value={filters.dateFrom} onChange={(event) => changeFilter("dateFrom", event.target.value)} />
</label>
<label>To
<input type="date" value={filters.dateTo} onChange={(event) => changeFilter("dateTo", event.target.value)} />
</label>
</div>
)}
<div className="mailbox-rows" aria-busy={status.loading}>
{status.loading && <div className="mailbox-state">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>
)}
{!status.loading && !status.error && listedEmails.map((email) => (
<MailRow
key={email.id}
email={email}
selected={selected === email.id}
checked={Boolean(checked[email.id])}
onOpen={openEmail}
onToggleStar={toggleStar}
onToggleCheck={(item) => setChecked((current) => ({ ...current, [item.id]: !current[item.id] }))}
onTrash={removeEmail}
onMarkUnread={markUnread}
onArchive={(item) => hideForSession(item, "Archived in this view until you refresh.")}
onSnooze={(item) => hideForSession(item, "Snoozed in this view until you refresh.")}
onSummarize={summarize}
/>
))}
</div>
{(cursorHistory.length > 0 || nextCursor) && (
<footer className="mailbox-pagination">
<button type="button" disabled={!cursorHistory.length} onClick={previousPage}><ChevronLeft size={16} /> Previous</button>
<span>Page {cursorHistory.length + 1}</span>
<button type="button" disabled={!nextCursor} onClick={nextPage}>Next <ChevronRight size={16} /></button>
</footer>
)}
<MailDetail
email={detail}
loading={detailStatus.loading}
error={detailStatus.error}
busy={detailStatus.busy}
mailbox={mailbox}
onClose={() => { setSelected(null); setDetail(null); }}
onToggleRead={toggleRead}
onTrash={() => mutateSelected(trashEmail)}
onRestore={() => mutateSelected(restoreEmail)}
onDelete={deletePermanently}
/>
</div>
)}
{!status.loading && !status.error && emails.length === 0 && (
<div className="mailbox-state empty">
<span><Inbox size={22} /></span>
<strong>No messages here</strong>
<p>{hasFilters || filters.query ? "Try changing your search or filters." : `Your ${title.toLowerCase()} is empty.`}</p>
</div>
)}
{!status.loading && !status.error && emails.map((email) => (
<MailRow
key={email.id}
email={email}
selected={selected === email.id}
onOpen={openEmail}
onToggleStar={toggleStar}
/>
))}
</div>
{!status.loading && !status.error && emails.length > 0 && (
<footer className="mailbox-list-footer">
<p>
{emails.length} message{emails.length === 1 ? "" : "s"} in this view
{filters.query.trim() ? " · filtered" : ""}
</p>
<span>You're all caught up</span>
</footer>
)}
{(cursorHistory.length > 0 || nextCursor) && (
<footer className="mailbox-pagination">
<button type="button" disabled={!cursorHistory.length} onClick={previousPage}><ChevronLeft size={16} /> Previous</button>
<span>Page {cursorHistory.length + 1}</span>
<button type="button" disabled={!nextCursor} onClick={nextPage}>Next <ChevronRight size={16} /></button>
</footer>
)}
<MailDetail
email={detail}
loading={detailStatus.loading}
error={detailStatus.error}
busy={detailStatus.busy}
mailbox={mailbox}
onClose={() => { setSelected(null); setDetail(null); }}
onToggleRead={toggleRead}
onTrash={() => mutateSelected(trashEmail)}
onRestore={() => mutateSelected(restoreEmail)}
onDelete={deletePermanently}
/>
</div>
</div>
<aside className="mailbox-rail">
<AgAgentPanel
account={account}
emails={listedEmails}
focus={agentFocus}
onQuickAction={runQuickAction}
/>
<StickyNotesPanel compact />
</aside>
</div>
<StickyNotesPanel />
</section>
);
}

View file

@ -78,7 +78,7 @@ function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
);
}
export default function StickyNotesPanel() {
export default function StickyNotesPanel({ compact = false }) {
const token = sessionStorage.getItem("agmail_access_token");
const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true);
@ -178,7 +178,7 @@ export default function StickyNotesPanel() {
}
return (
<aside className="mailbox-notes-pane" aria-label="Sticky notes">
<aside className={`mailbox-notes-pane${compact ? " compact" : ""}`} aria-label="Sticky notes">
<header className="sticky-notes-header">
<div className="sticky-notes-title">
<Pin size={15} />