UI enhancement

This commit is contained in:
debu-2 2026-10-03 22:34:34 +05:30
parent 23239c5cb6
commit 53922952c6
13 changed files with 1076 additions and 260 deletions

View file

@ -1,13 +1,17 @@
import { import {
Calendar, Calendar,
CalendarClock,
FileSearch, FileSearch,
ListChecks, ListChecks,
PenLine, MessageCircle,
Receipt, Receipt,
Sparkles,
Star, Star,
} from "lucide-react"; } from "lucide-react";
import { inboxIntelligence } from "../utils/inboxSignals"; import AgentInsight from "./AgentInsight";
import AgentSpark from "./AgentSpark";
import { inboxContext } from "../utils/inboxSignals";
const ICON = { size: 16, strokeWidth: 1.75 };
function greeting(name) { function greeting(name) {
const hour = new Date().getHours(); const hour = new Date().getHours();
@ -16,44 +20,28 @@ function greeting(name) {
} }
const cards = [ const cards = [
{ { key: "important", label: "Important", hint: "Need a look", icon: Star, tone: "rose" },
key: "important", { key: "actionItems", label: "Actions", hint: "Need response", icon: ListChecks, tone: "blue" },
label: "Important emails", { key: "meetings", label: "Meetings", hint: "Mentioned", icon: Calendar, tone: "green" },
hint: "Need a look", { key: "invoices", label: "Invoices", hint: "This week", icon: Receipt, tone: "amber" },
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 }) { export default function AgAgentPanel({
const stats = inboxIntelligence(emails); account,
emails,
focus,
insight,
onQuickAction,
onOpenEmail,
}) {
const { stats, needsReply, today, deadlines } = inboxContext(emails);
const firstName = account?.first_name || "there"; const firstName = account?.first_name || "there";
const hasSignals = stats.important + stats.actionItems + stats.meetings + stats.invoices > 0;
return ( return (
<section className="agent-panel" aria-label="AG Agent"> <section className="agent-panel" aria-label="AG Agent">
<header className="agent-panel-head"> <header className="agent-panel-head">
<span className="agent-panel-mark" aria-hidden="true"><Sparkles size={15} /></span> <span className="agent-panel-mark" aria-hidden="true"><AgentSpark /></span>
<div> <div>
<h2>AG Agent</h2> <h2>AG Agent</h2>
<p>Your email co-pilot</p> <p>Your email co-pilot</p>
@ -62,49 +50,113 @@ export default function AgAgentPanel({ account, emails, focus, onQuickAction })
<div className="agent-panel-hello"> <div className="agent-panel-hello">
<strong>{greeting(firstName)}</strong> <strong>{greeting(firstName)}</strong>
<span>Here&apos;s what needs your attention.</span> {!insight && (
{focus && <p>{focus}</p>} hasSignals ? (
<span>Here&apos;s what needs your attention.</span>
) : (
<span>You&apos;re all caught up. No urgent messages or outstanding actions.</span>
)
)}
{!insight && focus && <p>{focus}</p>}
</div> </div>
<div className="agent-stat-grid"> <AgentInsight insight={insight} />
{cards.map((card) => {
const Icon = card.icon; {!insight && hasSignals ? (
const count = stats[card.key] || 0; <div className="agent-stat-grid">
return ( {cards.map((card) => {
<button const Icon = card.icon;
key={card.key} const count = stats[card.key] || 0;
type="button" return (
className={`agent-stat agent-stat-${card.tone}`} <button
onClick={() => onQuickAction(card.key)} key={card.key}
> type="button"
<Icon size={15} /> className={`agent-stat agent-stat-${card.tone}`}
<strong>{count}</strong> onClick={() => onQuickAction(card.key)}
<span>{card.label}</span> >
<small>{card.key === "important" && stats.importantFromClients <Icon {...ICON} />
? `${stats.importantFromClients} from clients` <strong>{count}</strong>
: card.hint}</small> <span>{card.label}</span>
<small>{card.key === "important" && stats.importantFromClients
? `${stats.importantFromClients} from clients`
: card.hint}</small>
</button>
);
})}
</div>
) : !insight && (
<div className="agent-caught-up">
<MessageCircle {...ICON} />
<p>Inbox is quiet right now.</p>
{stats.unread > 0 && (
<button type="button" className="agent-mini-metric" onClick={() => onQuickAction("unread")}>
{stats.unread} unread
</button> </button>
); )}
})} </div>
</div> )}
<div className="agent-quick"> <div className="agent-quick">
<h3>Quick actions</h3> <h3>Quick actions</h3>
<div> <div>
<button type="button" onClick={() => onQuickAction("summarize")}> <button type="button" className="agent-action subtle" onClick={() => onQuickAction("summarize")}>
<Sparkles size={14} /> Summarize inbox <AgentSpark /> Summarize inbox
</button> </button>
<button type="button" onClick={() => onQuickAction("draft")}> <button type="button" className="agent-action subtle" onClick={() => onQuickAction("draft")}>
<PenLine size={14} /> Draft replies <AgentSpark /> Draft replies
</button> </button>
<button type="button" onClick={() => onQuickAction("attachments")}> <button type="button" onClick={() => onQuickAction("attachments")}>
<FileSearch size={14} /> Find attachments <FileSearch {...ICON} /> Find attachments
</button> </button>
<button type="button" onClick={() => onQuickAction("actions")}> <button type="button" onClick={() => onQuickAction("actions")}>
<ListChecks size={14} /> Show action items <ListChecks {...ICON} /> Show action items
</button> </button>
</div> </div>
</div> </div>
{!insight && today.length > 0 && (
<section className="agent-context-block">
<h3>Today</h3>
<ul>
{today.map((item) => (
<li key={`${item.time}-${item.label}`}>
<time>{item.time}</time>
<span>{item.label}</span>
</li>
))}
</ul>
</section>
)}
{!insight && needsReply.length > 0 && (
<section className="agent-context-block">
<h3>Needs reply</h3>
<ul>
{needsReply.map((item) => (
<li key={item.id}>
<button type="button" onClick={() => onOpenEmail?.(item.id)}>
<span>{item.name}</span>
<em>{item.age}</em>
</button>
</li>
))}
</ul>
</section>
)}
{!insight && deadlines.length > 0 && (
<section className="agent-context-block">
<h3><CalendarClock size={16} strokeWidth={1.75} /> Deadlines</h3>
<ul>
{deadlines.map((item) => (
<li key={`${item.label}-${item.due}`}>
<span>{item.label}</span>
<em>{item.due}</em>
</li>
))}
</ul>
</section>
)}
</section> </section>
); );
} }

View file

@ -0,0 +1,48 @@
import AgentSpark from "./AgentSpark";
export default function AgentInsight({ insight }) {
if (!insight) return null;
const showSummary = insight.summary && insight.phase !== "understanding";
const showDetails = insight.details?.length > 0 && ["details", "done"].includes(insight.phase);
const showActions = insight.actions?.length > 0 && insight.phase === "done";
return (
<section className="agent-insight" aria-live="polite" aria-busy={insight.processing}>
{insight.processing && insight.phase === "understanding" && (
<p className="agent-insight-status">
<AgentSpark pulse /> Understanding conversation…
</p>
)}
{showSummary && (
<div className="agent-insight-block reveal">
<h4><AgentSpark /> Summary</h4>
<p>{insight.summary}</p>
</div>
)}
{showDetails && (
<div className="agent-insight-block reveal delay-1">
<h4>Extracted details</h4>
<ul>
{insight.details.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
)}
{showActions && (
<div className="agent-insight-block reveal delay-2">
<h4>Suggested actions</h4>
<ul className="agent-insight-actions">
{insight.actions.map((item) => (
<li key={item}><AgentSpark /> {item}</li>
))}
</ul>
</div>
)}
</section>
);
}

View file

@ -0,0 +1,18 @@
export default function AgentSpark({ pulse = false, className = "" }) {
return (
<span
className={`agent-spark${pulse ? " pulse" : ""}${className ? ` ${className}` : ""}`}
aria-hidden="true"
>
✦
</span>
);
}
export function AgentLabel({ children, className = "" }) {
return (
<span className={`agent-label${className ? ` ${className}` : ""}`}>
<AgentSpark /> {children}
</span>
);
}

View file

@ -0,0 +1,128 @@
import {
FileSearch,
ListChecks,
Mail,
Receipt,
Search,
Sparkles,
UserSearch,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
const COMMANDS = [
{ id: "search", label: "Search mail", hint: "Find messages by keyword", icon: Search, action: "search" },
{ id: "ask", label: "✦ Ask AG Agent", hint: "Summarize what needs attention", icon: Sparkles, action: "summarize", agent: true },
{ id: "attachments", label: "Find attachments", hint: "Messages with files", icon: FileSearch, action: "attachments" },
{ id: "from", label: "Find emails from…", hint: "Filter by sender", icon: UserSearch, action: "from" },
{ id: "invoices", label: "Show invoices", hint: "Finance category", icon: Receipt, action: "invoices" },
{ id: "unread", label: "Show unread", hint: "Unread messages only", icon: Mail, action: "unread" },
{ id: "replies", label: "Show emails needing replies", hint: "Action required", icon: ListChecks, action: "actions" },
];
export default function CommandPalette({ open, onClose, onCommand }) {
const [query, setQuery] = useState("");
const [active, setActive] = useState(0);
const filtered = useMemo(() => {
const needle = query.trim().toLowerCase();
if (!needle) return COMMANDS;
return COMMANDS.filter(
(command) =>
command.label.toLowerCase().includes(needle)
|| command.hint.toLowerCase().includes(needle),
);
}, [query]);
useEffect(() => {
if (!open) {
setQuery("");
setActive(0);
return undefined;
}
function onKeyDown(event) {
if (event.key === "Escape") {
event.preventDefault();
onClose();
return;
}
if (event.key === "ArrowDown") {
event.preventDefault();
setActive((index) => Math.min(index + 1, filtered.length - 1));
return;
}
if (event.key === "ArrowUp") {
event.preventDefault();
setActive((index) => Math.max(index - 1, 0));
return;
}
if (event.key === "Enter" && filtered[active]) {
event.preventDefault();
onCommand(filtered[active].action, query);
onClose();
}
}
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [active, filtered, onClose, onCommand, open, query]);
useEffect(() => {
setActive(0);
}, [query]);
if (!open) return null;
return (
<div className="command-palette-overlay" role="presentation" onClick={onClose}>
<div
className="command-palette"
role="dialog"
aria-modal="true"
aria-label="Command palette"
onClick={(event) => event.stopPropagation()}
>
<label className="command-palette-input">
<Search size={16} />
<input
autoFocus
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Search emails, people, or ask your agent…"
/>
<kbd>esc</kbd>
</label>
<ul className="command-palette-list">
{filtered.map((command, index) => {
const Icon = command.icon;
return (
<li key={command.id}>
<button
type="button"
className={index === active ? "active" : ""}
onMouseEnter={() => setActive(index)}
onClick={() => {
onCommand(command.action, query);
onClose();
}}
>
<Icon size={15} />
<span>
<strong>{command.label}</strong>
<small>{command.hint}</small>
</span>
</button>
</li>
);
})}
{filtered.length === 0 && (
<li className="command-palette-empty">No matching commands</li>
)}
</ul>
<footer className="command-palette-foot">
<span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
<span><kbd>↵</kbd> run</span>
<span><kbd>/</kbd> or <kbd>Ctrl K</kbd> open</span>
</footer>
</div>
</div>
);
}

View file

@ -4,10 +4,10 @@ import {
Link, Link,
List, List,
Send, Send,
Sparkles,
Underline, Underline,
X, X,
} from "lucide-react"; } from "lucide-react";
import AgentSpark from "./AgentSpark";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { sendEmail } from "../api"; import { sendEmail } from "../api";
@ -86,7 +86,7 @@ export default function ComposeModal({ account, onClose, onSent }) {
<section className="compose-modal" role="dialog" aria-modal="true" aria-labelledby="compose-title"> <section className="compose-modal" role="dialog" aria-modal="true" aria-labelledby="compose-title">
<header className="compose-header"> <header className="compose-header">
<div> <div>
<span><Sparkles size={14} /></span> <span className="compose-agent-mark"><AgentSpark /></span>
<div><h2 id="compose-title">New message</h2><small>From {account.email}</small></div> <div><h2 id="compose-title">New message</h2><small>From {account.email}</small></div>
</div> </div>
<button type="button" aria-label="Close compose" onClick={onClose} disabled={status.sending}> <button type="button" aria-label="Close compose" onClick={onClose} disabled={status.sending}>
@ -135,7 +135,7 @@ export default function ComposeModal({ account, onClose, onSent }) {
</div> </div>
<div className="compose-ai"> <div className="compose-ai">
<Sparkles size={18} /> <AgentSpark />
<input <input
value={aiPrompt} value={aiPrompt}
onChange={(event) => setAiPrompt(event.target.value)} onChange={(event) => setAiPrompt(event.target.value)}
@ -147,8 +147,8 @@ export default function ComposeModal({ account, onClose, onSent }) {
}} }}
placeholder="Describe what you want to say" placeholder="Describe what you want to say"
/> />
<button type="button" onClick={helpWrite} disabled={!aiPrompt.trim()}> <button type="button" className="agent-action" onClick={helpWrite} disabled={!aiPrompt.trim()}>
Help me write <AgentSpark /> Improve
</button> </button>
</div> </div>

View file

@ -6,8 +6,13 @@ import {
Trash2, Trash2,
X, X,
} from "lucide-react"; } from "lucide-react";
import AgentSpark from "./AgentSpark";
import SenderAvatar from "./SenderAvatar"; import SenderAvatar from "./SenderAvatar";
import { senderDisplayName } from "../utils/senderAvatar"; import { senderDisplayName } from "../utils/senderAvatar";
import { attachmentCount } from "../utils/inboxSignals";
const ICON = { size: 18, strokeWidth: 1.75 };
const ICON_SM = { size: 16, strokeWidth: 1.75 };
function joinAddresses(value) { function joinAddresses(value) {
if (!value) return ""; if (!value) return "";
@ -41,12 +46,17 @@ export default function MailDetail({
onTrash, onTrash,
onRestore, onRestore,
onDelete, onDelete,
onExtractActions,
onSummarizeThread,
onAnalyzeAttachment,
}) { }) {
const files = email ? attachmentCount(email) : 0;
return ( return (
<article className={`mail-detail${email || loading ? " open" : ""}`} aria-live="polite"> <article className={`mail-detail${email || loading ? " open" : ""}`} aria-live="polite">
<header className="mail-detail-toolbar"> <header className="mail-detail-toolbar">
<button type="button" onClick={onClose} aria-label="Back to message list"> <button type="button" onClick={onClose} aria-label="Back to message list">
<ArrowLeft size={18} /> <ArrowLeft {...ICON} />
</button> </button>
<div> <div>
{email && ( {email && (
@ -57,12 +67,12 @@ export default function MailDetail({
title={email.is_read ? "Mark unread" : "Mark read"} title={email.is_read ? "Mark unread" : "Mark read"}
aria-label={email.is_read ? "Mark unread" : "Mark read"} aria-label={email.is_read ? "Mark unread" : "Mark read"}
> >
{email.is_read ? <Mail size={17} /> : <MailOpen size={17} />} {email.is_read ? <Mail {...ICON} /> : <MailOpen {...ICON} />}
</button> </button>
)} )}
{email && mailbox === "trash" && ( {email && mailbox === "trash" && (
<button type="button" disabled={busy} onClick={onRestore} title="Restore" aria-label="Restore message"> <button type="button" disabled={busy} onClick={onRestore} title="Restore" aria-label="Restore message">
<RotateCcw size={17} /> <RotateCcw {...ICON} />
</button> </button>
)} )}
{email && ( {email && (
@ -74,11 +84,11 @@ export default function MailDetail({
title={mailbox === "trash" ? "Delete permanently" : "Move to trash"} title={mailbox === "trash" ? "Delete permanently" : "Move to trash"}
aria-label={mailbox === "trash" ? "Delete message permanently" : "Move message to trash"} aria-label={mailbox === "trash" ? "Delete message permanently" : "Move message to trash"}
> >
<Trash2 size={17} /> <Trash2 {...ICON} />
</button> </button>
)} )}
<button className="mail-detail-close" type="button" onClick={onClose} aria-label="Close message"> <button className="mail-detail-close" type="button" onClick={onClose} aria-label="Close message">
<X size={18} /> <X size={18} strokeWidth={1.75} />
</button> </button>
</div> </div>
</header> </header>
@ -87,6 +97,19 @@ export default function MailDetail({
{error && <div className="mail-detail-state error" role="alert">{error}</div>} {error && <div className="mail-detail-state error" role="alert">{error}</div>}
{email && !loading && ( {email && !loading && (
<div className="mail-detail-content"> <div className="mail-detail-content">
<div className="mail-detail-agent-bar">
<button type="button" className="agent-action subtle" onClick={onExtractActions}>
<AgentSpark /> Extract actions
</button>
<button type="button" className="agent-action subtle" onClick={onSummarizeThread}>
<AgentSpark /> Summarize thread
</button>
{files > 0 && (
<button type="button" className="agent-action subtle" onClick={onAnalyzeAttachment}>
<AgentSpark /> Analyze attachment
</button>
)}
</div>
<h2>{email.subject?.trim() || "(No subject)"}</h2> <h2>{email.subject?.trim() || "(No subject)"}</h2>
<div className="mail-detail-meta"> <div className="mail-detail-meta">
<div> <div>

View file

@ -3,14 +3,16 @@ import {
Clock3, Clock3,
MailOpen, MailOpen,
Paperclip, Paperclip,
Sparkles,
Star, Star,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import AgentSpark from "./AgentSpark";
import SenderAvatar from "./SenderAvatar"; import SenderAvatar from "./SenderAvatar";
import { senderDisplayName } from "../utils/senderAvatar"; import { senderDisplayName } from "../utils/senderAvatar";
import { attachmentCount, emailLabels } from "../utils/inboxSignals"; import { attachmentCount, emailLabels } from "../utils/inboxSignals";
const ICON = { size: 16, strokeWidth: 1.75 };
function formatTimestamp(value) { function formatTimestamp(value) {
if (!value) return ""; if (!value) return "";
const date = new Date(value); const date = new Date(value);
@ -65,14 +67,23 @@ export default function MailRow({
action?.(); action?.();
} }
const rowClass = [
"mailbox-row",
unread && "unread",
selected && "selected",
checked && "checked",
starred && "starred",
].filter(Boolean).join(" ");
return ( return (
<div <div
className={`mailbox-row${unread ? " unread" : ""}${selected ? " selected" : ""}${starred ? " starred" : ""}`} className={rowClass}
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => onOpen(email)} onClick={() => onOpen(email)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-label={`${unread ? "Unread" : "Read"} email from ${senderName}: ${subject}`} aria-label={`${unread ? "Unread" : "Read"} email from ${senderName}: ${subject}`}
aria-selected={selected}
> >
<input <input
className="mailbox-check" className="mailbox-check"
@ -89,7 +100,7 @@ export default function MailRow({
aria-label={starred ? "Unstar message" : "Star message"} aria-label={starred ? "Unstar message" : "Star message"}
aria-pressed={starred} aria-pressed={starred}
> >
<Star size={14} fill={starred ? "currentColor" : "none"} /> <Star size={16} strokeWidth={1.75} fill={starred ? "currentColor" : "none"} />
</button> </button>
<SenderAvatar email={email} className="mailbox-avatar" /> <SenderAvatar email={email} className="mailbox-avatar" />
<div className="mailbox-content"> <div className="mailbox-content">
@ -109,7 +120,7 @@ export default function MailRow({
))} ))}
{files > 0 && ( {files > 0 && (
<em className="mail-attachment" title={`${files} attachment${files === 1 ? "" : "s"}`}> <em className="mail-attachment" title={`${files} attachment${files === 1 ? "" : "s"}`}>
<Paperclip size={11} /> <Paperclip {...ICON} />
{files > 1 ? files : ""} {files > 1 ? files : ""}
</em> </em>
)} )}
@ -117,12 +128,29 @@ export default function MailRow({
)} )}
</div> </div>
<time className="mailbox-time" dateTime={timestamp || undefined}>{formatTimestamp(timestamp)}</time> <time className="mailbox-time" dateTime={timestamp || undefined}>{formatTimestamp(timestamp)}</time>
<div className="mailbox-hover-actions"> <div className="mailbox-hover-actions" aria-label="Message actions">
<button type="button" title="Archive" aria-label="Archive" onClick={(event) => stop(event, () => onArchive?.(email))}><Archive size={14} /></button> <button type="button" onClick={(event) => stop(event, () => onArchive?.(email))}>
<button type="button" title="Snooze" aria-label="Snooze" onClick={(event) => stop(event, () => onSnooze?.(email))}><Clock3 size={14} /></button> <Archive {...ICON} /> Archive
<button type="button" title="Mark unread" aria-label="Mark unread" onClick={(event) => stop(event, () => onMarkUnread?.(email))}><MailOpen size={14} /></button> </button>
<button type="button" title="Delete" aria-label="Delete" onClick={(event) => stop(event, () => onTrash?.(email))}><Trash2 size={14} /></button> <span className="mailbox-hover-sep" aria-hidden="true">·</span>
<button type="button" title="Summarize" aria-label="Summarize" onClick={(event) => stop(event, () => onSummarize?.(email))}><Sparkles size={14} /></button> <button type="button" onClick={(event) => stop(event, () => onTrash?.(email))}>
<Trash2 {...ICON} /> Delete
</button>
<span className="mailbox-hover-sep" aria-hidden="true">·</span>
<button type="button" onClick={(event) => stop(event, () => onMarkUnread?.(email))}>
<MailOpen {...ICON} /> Mark unread
</button>
<span className="mailbox-hover-sep" aria-hidden="true">·</span>
<button type="button" onClick={(event) => stop(event, () => onSnooze?.(email))}>
<Clock3 {...ICON} /> Snooze
</button>
<button
type="button"
className="agent-action"
onClick={(event) => stop(event, () => onSummarize?.(email))}
>
<AgentSpark /> Summarize
</button>
</div> </div>
</div> </div>
); );

View file

@ -0,0 +1,56 @@
import {
Archive,
Clock3,
FolderInput,
MailOpen,
MoreHorizontal,
Tag,
Trash2,
} from "lucide-react";
import AgentSpark from "./AgentSpark";
const ICON = { size: 16, strokeWidth: 1.75 };
export default function MailboxSelectionBar({
count,
total,
onToggleAll,
onArchive,
onSpam,
onDelete,
onMarkUnread,
onSnooze,
onMove,
onLabel,
onMore,
onSummarize,
}) {
const allSelected = total > 0 && count === total;
return (
<div className="mailbox-selection-bar" role="toolbar" aria-label="Bulk message actions">
<div className="mailbox-selection-meta">
<input
type="checkbox"
checked={allSelected}
aria-label={allSelected ? "Deselect all messages" : "Select all messages"}
onChange={onToggleAll}
/>
<span>{count} selected</span>
</div>
<div className="mailbox-selection-actions">
<button type="button" onClick={onArchive}><Archive {...ICON} /> Archive</button>
<button type="button" onClick={onSpam}><Trash2 {...ICON} /> Spam</button>
<button type="button" onClick={onDelete}><Trash2 {...ICON} /> Delete</button>
<button type="button" onClick={onMarkUnread}><MailOpen {...ICON} /> Mark unread</button>
<button type="button" onClick={onSnooze}><Clock3 {...ICON} /> Snooze</button>
<button type="button" onClick={onMove}><FolderInput {...ICON} /> Move</button>
<button type="button" onClick={onLabel}><Tag {...ICON} /> Label</button>
<button type="button" onClick={onMore}><MoreHorizontal {...ICON} /> More</button>
</div>
<button type="button" className="mailbox-selection-summarize agent-action" onClick={onSummarize}>
<AgentSpark /> Summarize
</button>
</div>
);
}

View file

@ -11,7 +11,6 @@ import {
RefreshCw, RefreshCw,
Search, Search,
Settings, Settings,
Sparkles,
Sun, Sun,
Tag, Tag,
Users, Users,
@ -28,8 +27,12 @@ import {
trashEmail, trashEmail,
} from "../api"; } from "../api";
import AgAgentPanel from "./AgAgentPanel"; import AgAgentPanel from "./AgAgentPanel";
import AgentSpark from "./AgentSpark";
import CommandPalette from "./CommandPalette";
import { useAgentInsight } from "../hooks/useAgentInsight";
import MailDetail from "./MailDetail"; import MailDetail from "./MailDetail";
import MailRow from "./MailRow"; import MailRow from "./MailRow";
import MailboxSelectionBar from "./MailboxSelectionBar";
import StickyNotesPanel from "./StickyNotesPanel"; import StickyNotesPanel from "./StickyNotesPanel";
import { import {
categoryCounts, categoryCounts,
@ -82,6 +85,8 @@ export default function MailboxView({
const [filtersOpen, setFiltersOpen] = useState(false); const [filtersOpen, setFiltersOpen] = useState(false);
const [category, setCategory] = useState("Primary"); const [category, setCategory] = useState("Primary");
const [agentFocus, setAgentFocus] = useState(""); const [agentFocus, setAgentFocus] = useState("");
const { insight, runInsight } = useAgentInsight();
const [commandOpen, setCommandOpen] = useState(false);
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
query: "", query: "",
read: "all", read: "all",
@ -121,15 +126,27 @@ export default function MailboxView({
}, [load, refreshKey]); }, [load, refreshKey]);
useEffect(() => { useEffect(() => {
function focusSearch(event) { function openPalette(event) {
const target = event.target;
const typing =
target instanceof HTMLInputElement
|| target instanceof HTMLTextAreaElement
|| target instanceof HTMLSelectElement
|| target?.isContentEditable;
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
event.preventDefault(); event.preventDefault();
document.getElementById("workspace-search")?.focus(); setCommandOpen(true);
return;
}
if (event.key === "/" && !typing && !commandOpen) {
event.preventDefault();
setCommandOpen(true);
} }
} }
window.addEventListener("keydown", focusSearch); window.addEventListener("keydown", openPalette);
return () => window.removeEventListener("keydown", focusSearch); return () => window.removeEventListener("keydown", openPalette);
}, []); }, [commandOpen]);
const visibleEmails = useMemo( const visibleEmails = useMemo(
() => emails.filter((email) => !hidden[email.id] && matchesLens(email, lens)), () => emails.filter((email) => !hidden[email.id] && matchesLens(email, lens)),
@ -142,6 +159,11 @@ export default function MailboxView({
}, [category, mailbox, visibleEmails]); }, [category, mailbox, visibleEmails]);
const counts = useMemo(() => categoryCounts(visibleEmails), [visibleEmails]); const counts = useMemo(() => categoryCounts(visibleEmails), [visibleEmails]);
const selectedIds = useMemo(
() => Object.keys(checked).filter((id) => checked[id]),
[checked],
);
const selectedCount = selectedIds.length;
useEffect(() => { useEffect(() => {
if (!onStats) return; if (!onStats) return;
@ -246,24 +268,133 @@ export default function MailboxView({
function summarize(email) { function summarize(email) {
const subject = email.subject?.trim() || "(No subject)"; const subject = email.subject?.trim() || "(No subject)";
const snippet = email.snippet || email.preview || email.body_text || "No preview available."; const snippet = email.snippet || email.preview || email.body_text || "No preview available.";
setAgentFocus(`${subject} — ${snippet}`); setAgentFocus("");
runInsight({
title: subject,
summary: snippet,
details: [
`From ${email.sender?.name || email.sender?.email || "unknown sender"}`,
email.is_read === false ? "Marked unread — likely needs attention" : "Already read",
],
actions: ["Draft a reply", "Extract action items", "Snooze for later"],
});
}
function summarizeSelection() {
const items = listedEmails.filter((email) => checked[email.id]);
if (!items.length) return;
setAgentFocus("");
runInsight({
title: `${items.length} selected messages`,
summary: items.length === 1
? (items[0].snippet || items[0].preview || "No preview available.")
: `${items.length} conversations span ${items.filter((email) => email.is_read === false).length} unread threads.`,
details: items.slice(0, 3).map((email) => email.subject || "(No subject)"),
actions: ["Draft batch replies", "Archive completed threads", "Flag for follow-up"],
});
}
function runBulkAction(action) {
const items = listedEmails.filter((email) => checked[email.id]);
if (!items.length) return;
if (action === "archive" || action === "snooze") {
items.forEach((email) => hideForSession(
email,
action === "archive"
? "Archived in this view until you refresh."
: "Snoozed in this view until you refresh.",
));
}
if (action === "delete" || action === "spam") {
items.forEach((email) => removeEmail(email));
}
if (action === "unread") {
items.forEach((email) => markUnread(email));
}
if (action === "move" || action === "label" || action === "more") {
setAgentFocus(`${action === "move" ? "Move" : action === "label" ? "Label" : "More"} for ${items.length} selected messages is coming soon.`);
}
setChecked({});
}
function toggleSelectAll() {
if (selectedCount === listedEmails.length) {
setChecked({});
return;
}
setChecked(listedEmails.reduce((map, email) => ({ ...map, [email.id]: true }), {}));
}
function extractActions(email) {
const text = email.body_text || email.snippet || email.preview || "";
setAgentFocus("");
runInsight({
title: email.subject || "Message",
summary: text
? "Scanned the message for commitments, requests, and deadlines."
: "No plain-text content to extract actions from.",
details: text
? ["Reply requested or implied", "Review attachments if any", "Check calendar mentions"]
: ["Open the full message to extract richer actions"],
actions: ["Draft a reply", "Add to action items", "Set a reminder"],
});
}
function summarizeThread(email) {
setAgentFocus("");
runInsight({
title: email.subject || "Thread",
summary: email.snippet || email.preview || "Thread context loaded from the latest message.",
details: ["Prior messages in thread considered", "Open questions highlighted"],
actions: ["Draft a reply", "Summarize for your team", "Extract next steps"],
});
}
function analyzeAttachment(email) {
setAgentFocus("");
runInsight({
title: email.subject || "Attachments",
summary: "Reviewing files attached to this conversation.",
details: ["File types identified", "Key entities extracted from filenames"],
actions: ["Summarize document", "Find related messages", "Save to notes"],
});
} }
function runQuickAction(action) { function runQuickAction(action) {
const intel = inboxIntelligence(listedEmails); const intel = inboxIntelligence(listedEmails);
if (action === "summarize") { if (action === "summarize") {
setAgentFocus( setAgentFocus("");
listedEmails.length runInsight({
? `${listedEmails.length} messages in ${category}. ${intel.actionItems} look like they need a reply, ${intel.meetings} mention a meeting, and ${intel.invoices} look like bills.` title: `${category} inbox`,
summary: listedEmails.length
? `${listedEmails.length} messages loaded. ${intel.actionItems} look like they need a reply.`
: "Nothing in this view to summarize yet.", : "Nothing in this view to summarize yet.",
); details: [
`${intel.meetings} mention meetings`,
`${intel.invoices} look like bills`,
`${intel.unread} unread overall`,
],
actions: ["Show action items", "Draft replies", "Find attachments"],
});
return; return;
} }
if (action === "draft") { if (action === "draft") {
const first = listedEmails.find((email) => email.is_read === false) || listedEmails[0]; const first = listedEmails.find((email) => email.is_read === false) || listedEmails[0];
setAgentFocus(first setAgentFocus("");
? `Draft a reply to ${first.subject || "the latest message"} when you open compose.` runInsight({
: "No message is waiting for a reply."); title: first?.subject || "Draft replies",
summary: first
? `Ready to draft a reply to "${first.subject || "the latest message"}".`
: "No message is waiting for a reply.",
details: first ? ["Tone: professional and concise", "Context pulled from inbox signals"] : [],
actions: ["Open compose", "Suggest three reply options", "Improve existing draft"],
});
return;
}
if (action === "unread") {
changeFilter("read", "unread");
setFiltersOpen(true);
setAgentFocus(`${intel.unread} unread messages in this mailbox.`);
return; return;
} }
if (action === "attachments" || action === "actions" || action === "important" || action === "actionItems" || action === "meetings" || action === "invoices") { if (action === "attachments" || action === "actions" || action === "important" || action === "actionItems" || action === "meetings" || action === "invoices") {
@ -280,6 +411,21 @@ export default function MailboxView({
} }
} }
function handleCommand(action, query = "") {
if (action === "search") {
if (query.trim()) changeFilter("query", query.trim());
window.setTimeout(() => document.getElementById("workspace-search")?.focus(), 0);
return;
}
if (action === "from") {
setFiltersOpen(true);
if (query.trim()) changeFilter("sender", query.trim());
setAgentFocus("Find emails from a specific person — use the sender filter.");
return;
}
runQuickAction(action);
}
useEffect(() => { useEffect(() => {
if (!agentNav || status.loading) return; if (!agentNav || status.loading) return;
if (agentNav === "integrations") { if (agentNav === "integrations") {
@ -364,9 +510,16 @@ export default function MailboxView({
type="search" type="search"
value={filters.query} value={filters.query}
onChange={(event) => changeFilter("query", event.target.value)} onChange={(event) => changeFilter("query", event.target.value)}
placeholder="Search emails, people, or ask your agent..." placeholder="Search emails, people, or ask your agent…"
/> />
<kbd>Ctrl K</kbd> <button
type="button"
className="workspace-search-kbd"
aria-label="Open command palette"
onClick={() => setCommandOpen(true)}
>
<kbd>⌘K</kbd>
</button>
</label> </label>
</div> </div>
<div className="workspace-zone workspace-zone-actions"> <div className="workspace-zone workspace-zone-actions">
@ -379,8 +532,8 @@ export default function MailboxView({
> >
<Filter size={16} /> <Filter size={16} />
</button> </button>
<button className="workspace-ask" type="button" onClick={() => runQuickAction("summarize")}> <button className="workspace-ask agent-action" type="button" onClick={() => runQuickAction("summarize")}>
<Sparkles size={15} /> Ask Agent <AgentSpark /> Ask Agent
</button> </button>
</div> </div>
<div className="workspace-zone workspace-zone-account"> <div className="workspace-zone workspace-zone-account">
@ -403,7 +556,24 @@ export default function MailboxView({
<div className="mailbox-main-column"> <div className="mailbox-main-column">
<div className={`mailbox-shell${selected ? " detail-visible" : ""}`}> <div className={`mailbox-shell${selected ? " detail-visible" : ""}`}>
<div className="mailbox-list-pane"> <div className="mailbox-list-pane">
{mailbox === "inbox" && ( {selectedCount > 0 && (
<MailboxSelectionBar
count={selectedCount}
total={listedEmails.length}
onToggleAll={toggleSelectAll}
onArchive={() => runBulkAction("archive")}
onSpam={() => runBulkAction("spam")}
onDelete={() => runBulkAction("delete")}
onMarkUnread={() => runBulkAction("unread")}
onSnooze={() => runBulkAction("snooze")}
onMove={() => runBulkAction("move")}
onLabel={() => runBulkAction("label")}
onMore={() => runBulkAction("more")}
onSummarize={summarizeSelection}
/>
)}
{mailbox === "inbox" && selectedCount === 0 && (
<div className="mailbox-categories" role="tablist" aria-label="Inbox categories"> <div className="mailbox-categories" role="tablist" aria-label="Inbox categories">
{CATEGORY_TABS.map(({ name, icon: Icon }) => ( {CATEGORY_TABS.map(({ name, icon: Icon }) => (
<button <button
@ -414,7 +584,7 @@ export default function MailboxView({
className={category === name ? "active" : ""} className={category === name ? "active" : ""}
onClick={() => setCategory(name)} onClick={() => setCategory(name)}
> >
<Icon size={14} strokeWidth={1.75} /> <Icon size={16} strokeWidth={1.75} />
<span className="mailbox-category-label">{name}</span> <span className="mailbox-category-label">{name}</span>
{name !== "All Mail" && ( {name !== "All Mail" && (
<span className={`mailbox-category-count${counts[name] > 0 ? "" : " muted"}`}> <span className={`mailbox-category-count${counts[name] > 0 ? "" : " muted"}`}>
@ -507,6 +677,9 @@ export default function MailboxView({
onTrash={() => mutateSelected(trashEmail)} onTrash={() => mutateSelected(trashEmail)}
onRestore={() => mutateSelected(restoreEmail)} onRestore={() => mutateSelected(restoreEmail)}
onDelete={deletePermanently} onDelete={deletePermanently}
onExtractActions={() => detail && extractActions(detail)}
onSummarizeThread={() => detail && summarizeThread(detail)}
onAnalyzeAttachment={() => detail && analyzeAttachment(detail)}
/> />
</div> </div>
</div> </div>
@ -517,11 +690,22 @@ export default function MailboxView({
account={account} account={account}
emails={listedEmails} emails={listedEmails}
focus={agentFocus} focus={agentFocus}
insight={insight}
onQuickAction={runQuickAction} onQuickAction={runQuickAction}
onOpenEmail={(emailId) => {
const email = listedEmails.find((item) => item.id === emailId);
if (email) openEmail(email);
}}
/> />
<StickyNotesPanel compact /> <StickyNotesPanel compact contextEmail={detail} />
</aside> </aside>
</div> </div>
<CommandPalette
open={commandOpen}
onClose={() => setCommandOpen(false)}
onCommand={handleCommand}
/>
</section> </section>
); );
} }

View file

@ -1,10 +1,11 @@
import { import {
ChevronDown,
MoreHorizontal, MoreHorizontal,
Pin, Pin,
Plus, Plus,
Trash2, Trash2,
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
createStickyNote, createStickyNote,
deleteStickyNote, deleteStickyNote,
@ -14,6 +15,7 @@ import bluePin from "../data/blue.png";
import greenPin from "../data/green.png"; import greenPin from "../data/green.png";
import pinkPin from "../data/pink.png"; import pinkPin from "../data/pink.png";
import yellowPin from "../data/yellow.png"; import yellowPin from "../data/yellow.png";
import { senderDisplayName } from "../utils/senderAvatar";
const NOTE_COLORS = ["yellow", "green", "blue", "pink"]; const NOTE_COLORS = ["yellow", "green", "blue", "pink"];
const PIN_IMAGES = { const PIN_IMAGES = {
@ -35,6 +37,16 @@ function formatNoteTime(value) {
}).format(date); }).format(date);
} }
function noteMatchesContext(note, contextEmail) {
const needle = senderDisplayName(contextEmail).toLowerCase();
if (!needle) return false;
return (
note.title.toLowerCase().includes(needle)
|| note.body.toLowerCase().includes(needle)
|| note.tags.some((tag) => tag.toLowerCase().includes(needle))
);
}
function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) { function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
return ( return (
<article className={`sticky-note sticky-note-${note.color}`}> <article className={`sticky-note sticky-note-${note.color}`}>
@ -78,11 +90,12 @@ function NoteCard({ note, onDelete, menuOpen, onMenuToggle }) {
); );
} }
export default function StickyNotesPanel({ compact = false }) { export default function StickyNotesPanel({ compact = false, contextEmail = null }) {
const token = sessionStorage.getItem("agmail_access_token"); const token = sessionStorage.getItem("agmail_access_token");
const [notes, setNotes] = useState([]); const [notes, setNotes] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [expanded, setExpanded] = useState(false);
const [adding, setAdding] = useState(false); const [adding, setAdding] = useState(false);
const [menuOpenId, setMenuOpenId] = useState(null); const [menuOpenId, setMenuOpenId] = useState(null);
const [draft, setDraft] = useState({ const [draft, setDraft] = useState({
@ -93,6 +106,16 @@ export default function StickyNotesPanel({ compact = false }) {
}); });
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const contextualNotes = useMemo(() => {
if (!contextEmail) return [];
return notes.filter((note) => noteMatchesContext(note, contextEmail));
}, [contextEmail, notes]);
const visibleNotes = useMemo(() => {
if (contextEmail && contextualNotes.length) return contextualNotes;
return notes;
}, [contextEmail, contextualNotes, notes]);
const load = useCallback(async () => { const load = useCallback(async () => {
setLoading(true); setLoading(true);
setError(""); setError("");
@ -144,6 +167,7 @@ export default function StickyNotesPanel({ compact = false }) {
color: NOTE_COLORS[(notes.length + 1) % NOTE_COLORS.length], color: NOTE_COLORS[(notes.length + 1) % NOTE_COLORS.length],
}); });
setAdding(false); setAdding(false);
setExpanded(true);
} catch (createError) { } catch (createError) {
setError(createError.message); setError(createError.message);
} finally { } finally {
@ -177,107 +201,130 @@ export default function StickyNotesPanel({ compact = false }) {
}); });
} }
const contextLabel = contextEmail ? senderDisplayName(contextEmail) : null;
return ( return (
<aside className={`mailbox-notes-pane${compact ? " compact" : ""}`} aria-label="Sticky notes"> <aside className={`mailbox-notes-pane${compact ? " compact" : ""}${expanded ? " expanded" : ""}`} aria-label="Sticky notes">
<header className="sticky-notes-header"> <button
<div className="sticky-notes-title"> type="button"
<Pin size={15} /> className="sticky-notes-toggle"
<h2>Sticky Notes</h2> aria-expanded={expanded}
</div> onClick={() => setExpanded((open) => !open)}
<div className="sticky-notes-actions"> >
<button <Pin size={14} />
type="button" <span>
aria-label="Add sticky note" Notes ({notes.length})
onClick={() => { {contextLabel && contextualNotes.length > 0 && (
setDraft((current) => ({ <small> · {contextualNotes.length} about {contextLabel}</small>
...current, )}
color: NOTE_COLORS[notes.length % NOTE_COLORS.length], </span>
})); <ChevronDown size={15} className={expanded ? "open" : ""} />
setAdding(true); </button>
}}
>
<Plus size={18} />
</button>
</div>
</header>
<div className="sticky-notes-body"> {expanded && (
{loading && <div className="sticky-notes-state">Loading notes…</div>} <div className="sticky-notes-body">
{error && !loading && ( {contextLabel && (
<div className="sticky-notes-state error" role="alert">{error}</div> <p className="sticky-notes-context">
)} {contextualNotes.length
{!loading && notes.map((note) => ( ? `Notes about ${contextLabel}`
<NoteCard : `No notes linked to ${contextLabel} yet.`}
key={note.id} </p>
note={note} )}
menuOpen={menuOpenId === note.id}
onMenuToggle={(id) => setMenuOpenId((current) => (current === id ? null : id))}
onDelete={handleDelete}
/>
))}
{adding ? ( <div className="sticky-notes-toolbar">
<form className="sticky-note-form" onSubmit={handleCreate}> <button
<input type="button"
type="text" aria-label="Add sticky note"
value={draft.title} onClick={() => {
onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))} setDraft((current) => ({
placeholder="Note title" ...current,
autoFocus color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
required tags: contextLabel && !current.tags ? contextLabel : current.tags,
}));
setAdding(true);
}}
>
<Plus size={16} />
</button>
</div>
{loading && <div className="sticky-notes-state">Loading notes…</div>}
{error && !loading && (
<div className="sticky-notes-state error" role="alert">{error}</div>
)}
{!loading && visibleNotes.map((note) => (
<NoteCard
key={note.id}
note={note}
menuOpen={menuOpenId === note.id}
onMenuToggle={(id) => setMenuOpenId((current) => (current === id ? null : id))}
onDelete={handleDelete}
/> />
<textarea ))}
value={draft.body}
onChange={(event) => setDraft((current) => ({ ...current, body: event.target.value }))} {adding ? (
onKeyDown={handleBodyKeyDown} <form className="sticky-note-form" onSubmit={handleCreate}>
placeholder="Jot down a quick note…" <input
rows={3} type="text"
/> value={draft.title}
<input onChange={(event) => setDraft((current) => ({ ...current, title: event.target.value }))}
type="text" placeholder="Note title"
value={draft.tags} autoFocus
onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))} required
placeholder="Tags (comma separated)" />
/> <textarea
<div className="sticky-note-colors"> value={draft.body}
{NOTE_COLORS.map((color) => ( onChange={(event) => setDraft((current) => ({ ...current, body: event.target.value }))}
<button onKeyDown={handleBodyKeyDown}
key={color} placeholder="Jot down a quick note…"
type="button" rows={3}
className={`sticky-note-color${draft.color === color ? " active" : ""}`} />
data-color={color} <input
aria-label={`${color} note`} type="text"
onClick={() => setDraft((current) => ({ ...current, color }))} value={draft.tags}
/> onChange={(event) => setDraft((current) => ({ ...current, tags: event.target.value }))}
))} placeholder="Tags (comma separated)"
</div> />
<div className="sticky-note-form-actions"> <div className="sticky-note-colors">
<button type="button" onClick={() => setAdding(false)} disabled={saving}> {NOTE_COLORS.map((color) => (
Cancel <button
</button> key={color}
<button type="submit" disabled={saving || !draft.title.trim()}> type="button"
{saving ? "Saving…" : "Save note"} className={`sticky-note-color${draft.color === color ? " active" : ""}`}
</button> data-color={color}
</div> aria-label={`${color} note`}
</form> onClick={() => setDraft((current) => ({ ...current, color }))}
) : ( />
<button ))}
type="button" </div>
className="sticky-note-add" <div className="sticky-note-form-actions">
onClick={() => { <button type="button" onClick={() => setAdding(false)} disabled={saving}>
setDraft((current) => ({ Cancel
...current, </button>
color: NOTE_COLORS[notes.length % NOTE_COLORS.length], <button type="submit" disabled={saving || !draft.title.trim()}>
})); {saving ? "Saving…" : "Save note"}
setAdding(true); </button>
}} </div>
> </form>
<Plus size={20} /> ) : (
<span>Add sticky note</span> <button
<small>Jot down a quick note…</small> type="button"
</button> className="sticky-note-add"
)} onClick={() => {
</div> setDraft((current) => ({
...current,
color: NOTE_COLORS[notes.length % NOTE_COLORS.length],
tags: contextLabel || current.tags,
}));
setAdding(true);
}}
>
<Plus size={18} />
<span>Add note</span>
</button>
)}
</div>
)}
</aside> </aside>
); );
} }

View file

@ -0,0 +1,51 @@
import { useCallback, useEffect, useRef, useState } from "react";
export function useAgentInsight() {
const [insight, setInsight] = useState(null);
const timers = useRef([]);
const clearTimers = useCallback(() => {
timers.current.forEach((timer) => window.clearTimeout(timer));
timers.current = [];
}, []);
useEffect(() => () => clearTimers(), [clearTimers]);
const runInsight = useCallback((payload) => {
clearTimers();
const { title, summary, details = [], actions = [] } = payload;
setInsight({
processing: true,
phase: "understanding",
title,
summary: null,
details: [],
actions: [],
});
timers.current.push(window.setTimeout(() => {
setInsight((current) => current ? { ...current, phase: "summary", summary } : current);
}, 520));
timers.current.push(window.setTimeout(() => {
setInsight((current) => current ? { ...current, phase: "details", details } : current);
}, 960));
timers.current.push(window.setTimeout(() => {
setInsight((current) => current ? {
...current,
phase: "done",
processing: false,
actions,
} : current);
}, 1400));
}, [clearTimers]);
const clearInsight = useCallback(() => {
clearTimers();
setInsight(null);
}, [clearTimers]);
return { insight, runInsight, clearInsight };
}

View file

@ -1,6 +1,6 @@
@import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap");
:root { font-family:"DM Sans",sans-serif; color:#080821; background:#fffdfb; font-synthesis:none; --ink:#080821; --muted:#67677a; --coral:#ff806c; --blush:#fff3ef; --line:#eee9e5; } :root { font-family:"DM Sans",sans-serif; color:#080821; background:#fffdfb; font-synthesis:none; --ink:#080821; --muted:#67677a; --coral:#ff6a52; --blush:#fff3ef; --line:#eee9e5; --agent:#7c5cff; --agent-soft:#f3efff; --agent-border:#d8ccff; --info:#4d74c4; --info-soft:#eef4ff; --success:#3d8a5d; --success-soft:#eef8f2; --attention:#b7892d; --attention-soft:#fff8ea; --navy:#1b162d; --space-1:4px; --space-2:8px; --space-3:12px; --space-4:16px; --space-5:20px; --space-6:24px; --space-7:32px; --text-content:14px; --text-meta:12px; --text-control:13px; --line-content:1.45; --line-meta:1.4; --radius-xs:6px; --radius-sm:8px; --radius-md:10px; --radius-lg:12px; --radius-xl:16px; --radius-float:20px; --radius-pill:999px; --shadow-surface:0 1px 3px #08082108,0 10px 28px #0808210a; --shadow-row:0 1px 0 #ffffffb8 inset,0 2px 10px #0808210a; --icon-stroke:1.75; --motion-fast:140ms; --motion-base:180ms; --motion-slow:200ms; --ease-standard:cubic-bezier(.2,.8,.2,1); }
* { box-sizing:border-box; } * { box-sizing:border-box; }
html { scroll-behavior:smooth; } html { scroll-behavior:smooth; }
body { margin:0; min-width:320px; } body { margin:0; min-width:320px; }
@ -290,8 +290,8 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.mail-detail-meta time { white-space:nowrap; } .mail-detail-meta time { white-space:nowrap; }
.mail-detail-body { padding-top:23px; color:#383440; font-size:12px; line-height:1.75; white-space:pre-wrap; overflow-wrap:anywhere; } .mail-detail-body { padding-top:23px; color:#383440; font-size:12px; line-height:1.75; white-space:pre-wrap; overflow-wrap:anywhere; }
.compose-overlay { position:fixed; inset:0; z-index:40; padding:24px; display:flex; align-items:center; justify-content:center; background:#08082175; backdrop-filter:blur(5px); } .compose-overlay { position:fixed; inset:0; z-index:40; padding:var(--space-6); display:flex; align-items:center; justify-content:center; background:#08082175; backdrop-filter:blur(5px); animation:fadeIn var(--motion-base) var(--ease-standard); }
.compose-modal { width:min(760px,100%); max-height:calc(100vh - 48px); overflow:auto; border:1px solid #eee7e2; border-radius:22px; background:#fffefd; box-shadow:0 32px 90px #08082150; } .compose-modal { width:min(760px,100%); max-height:calc(100vh - 48px); overflow:auto; border:0; border-radius:var(--radius-float); background:#fffefd; box-shadow:0 32px 90px #08082150; animation:scaleIn var(--motion-slow) var(--ease-standard); }
.compose-header { min-height:76px; padding:0 24px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); background:linear-gradient(100deg,#fff6f2,#fffefd); } .compose-header { min-height:76px; padding:0 24px; display:flex; align-items:center; justify-content:space-between; border-bottom:1px solid var(--line); background:linear-gradient(100deg,#fff6f2,#fffefd); }
.compose-header>div { display:flex; align-items:center; gap:11px; } .compose-header>div { display:flex; align-items:center; gap:11px; }
.compose-header>div>span { width:34px; height:34px; display:grid; place-items:center; border-radius:11px; background:var(--ink); color:var(--coral); } .compose-header>div>span { width:34px; height:34px; display:grid; place-items:center; border-radius:11px; background:var(--ink); color:var(--coral); }
@ -324,7 +324,9 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.compose-tools>span { margin-left:7px; align-self:center; color:#aaa4ac; font-size:8px; } .compose-tools>span { margin-left:7px; align-self:center; color:#aaa4ac; font-size:8px; }
.compose-ai { min-height:58px; margin:14px 0 10px; padding:8px 9px 8px 14px; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:10px; border:1px solid #e9e4ef; border-radius:14px; background:linear-gradient(100deg,#f5f4fb,#fff6f2); } .compose-ai { min-height:58px; margin:14px 0 10px; padding:8px 9px 8px 14px; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:10px; border:1px solid #e9e4ef; border-radius:14px; background:linear-gradient(100deg,#f5f4fb,#fff6f2); }
.compose-ai>svg { color:#7463a8; } .compose-ai>svg { color:#7463a8; }
.compose-ai button { min-height:35px; padding:0 13px; border:0; border-radius:9px; background:var(--ink); color:white; font-size:10px; font-weight:700; } .compose-ai button { min-height:35px; padding:0 13px; border:0; border-radius:var(--radius-sm); background:var(--ink); color:white; font-size:10px; font-weight:700; }
.compose-ai button.agent-action { background:var(--agent-soft); color:#6d4aff; border:1px solid var(--agent-border); font-size:12px; }
.compose-ai button.agent-action:hover:not(:disabled) { background:#ebe4ff; color:#5a3fe0; }
.compose-ai button:disabled { opacity:.45; cursor:not-allowed; } .compose-ai button:disabled { opacity:.45; cursor:not-allowed; }
.compose-error { margin:8px 0; color:#b54f3d; font-size:10px; } .compose-error { margin:8px 0; color:#b54f3d; font-size:10px; }
.compose-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; } .compose-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; }
@ -444,7 +446,7 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.dashboard-sidebar-section { margin-top:18px; padding-top:16px; border-top:1px solid #ffffff0a; } .dashboard-sidebar-section { margin-top:18px; padding-top:16px; border-top:1px solid #ffffff0a; }
.dashboard-sidebar-section h2 { margin:0 0 6px; padding:0 8px; color:#6f6a7d; font-size:9px; font-weight:700; letter-spacing:1.4px; text-transform:uppercase; } .dashboard-sidebar-section h2 { margin:0 0 6px; padding:0 8px; color:#6f6a7d; font-size:9px; font-weight:700; letter-spacing:1.4px; text-transform:uppercase; }
.dashboard-sidebar-section nav { display:grid; gap:1px; } .dashboard-sidebar-section nav { display:grid; gap:1px; }
.dashboard-nav-item { min-height:32px; padding:0 8px; display:flex; align-items:center; gap:10px; border:0; border-radius:9px; background:transparent; color:#b9b4c6; font-size:12.5px; font-weight:500; text-align:left; transition:background .14s ease,color .14s ease,box-shadow .14s ease; } .dashboard-nav-item { min-height:36px; padding:0 var(--space-2); display:flex; align-items:center; gap:var(--space-3); border:0; border-radius:var(--radius-sm); background:transparent; color:#b9b4c6; font-size:var(--text-meta); font-weight:500; text-align:left; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
.dashboard-nav-item svg { flex-shrink:0; opacity:.92; } .dashboard-nav-item svg { flex-shrink:0; opacity:.92; }
.dashboard-nav-item span { flex:1; min-width:0; } .dashboard-nav-item span { flex:1; min-width:0; }
.dashboard-nav-item em { margin-left:auto; min-width:17px; height:17px; padding:0 5px; display:grid; place-items:center; border-radius:99px; background:#ff6a52; color:#fff; font-size:9px; font-style:normal; font-weight:700; line-height:1; box-shadow:0 1px 4px #ff6a5238; } .dashboard-nav-item em { margin-left:auto; min-width:17px; height:17px; padding:0 5px; display:grid; place-items:center; border-radius:99px; background:#ff6a52; color:#fff; font-size:9px; font-style:normal; font-weight:700; line-height:1; box-shadow:0 1px 4px #ff6a5238; }
@ -467,32 +469,55 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.dashboard-profile button:hover { background:#ffffff12; color:#fff; } .dashboard-profile button:hover { background:#ffffff12; color:#fff; }
.dashboard-content { padding:14px 16px 16px; background:#e4e1dc; } .dashboard-content { padding:14px 16px 16px; background:#e4e1dc; }
.dashboard-content-heading h1 { font-size:clamp(28px,3vw,36px); letter-spacing:-1.4px; } .dashboard-content-heading h1 { font-size:clamp(28px,3vw,36px); letter-spacing:-1.4px; }
.mailbox-workspace { min-height:calc(100vh - 28px); display:flex; flex-direction:column; gap:12px; } .mailbox-workspace { min-height:calc(100vh - 28px); display:flex; flex-direction:column; gap:var(--space-3); }
.workspace-header { min-height:58px; display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:0 20px; align-items:center; } .workspace-header { min-height:56px; display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:0 var(--space-5); align-items:center; }
.workspace-zone { display:flex; align-items:center; min-width:0; } .workspace-zone { display:flex; align-items:center; min-width:0; }
.workspace-zone-command { padding-right:8px; } .workspace-zone-command { padding-right:8px; }
.workspace-zone-command .workspace-search { width:min(100%,620px); max-width:620px; } .workspace-zone-command .workspace-search { width:min(100%,620px); max-width:620px; }
.workspace-zone-actions { gap:10px; padding:0 16px; border-left:1px solid #d5d0ca; border-right:1px solid #d5d0ca; } .workspace-zone-actions { gap:var(--space-2); padding:0 var(--space-4); border-left:1px solid #d5d0ca; border-right:1px solid #d5d0ca; }
.workspace-zone-account { gap:6px; padding-left:4px; } .workspace-zone-account { gap:var(--space-2); padding-left:var(--space-1); }
.workspace-search { height:40px; min-width:0; padding:0 8px 0 14px; display:flex; align-items:center; gap:8px; border:1px solid #d5d0ca; border-radius:12px; background:#f8f7f5; color:#9a958f; box-shadow:none; } .workspace-search { height:40px; min-width:0; padding:0 var(--space-2) 0 var(--space-4); display:flex; align-items:center; gap:var(--space-2); border:1px solid #d5d0ca; border-radius:var(--radius-lg); background:#f8f7f5; color:#9a958f; box-shadow:none; transition:border-color var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
.workspace-search input { min-width:0; width:100%; border:0; outline:0; background:transparent; color:#241f2b; font-size:13px; } .workspace-search:focus-within { border-color:#d8ccff; box-shadow:0 0 0 3px #7c5cff14; }
.workspace-search kbd { padding:3px 7px; border:1px solid #e8e3de; border-radius:7px; background:#f2f0ec; color:#8d8792; font:600 10px "DM Sans"; white-space:nowrap; } .workspace-search input { min-width:0; width:100%; border:0; outline:0; background:transparent; color:#241f2b; font-size:var(--text-control); }
.workspace-filter,.workspace-icon { width:36px; height:36px; display:grid; place-items:center; border:0; border-radius:10px; background:transparent; color:#6f6a74; } .workspace-search kbd { padding:var(--space-1) var(--space-2); border:1px solid #e8e3de; border-radius:var(--radius-xs); background:#f2f0ec; color:#8d8792; font:600 var(--text-meta) "DM Sans"; white-space:nowrap; }
.workspace-filter,.workspace-icon { width:40px; height:40px; display:grid; place-items:center; border:0; border-radius:var(--radius-md); background:transparent; color:#6f6a74; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
.workspace-filter.active,.workspace-icon:hover,.workspace-filter:hover { background:#f8f6f3; color:#241f2b; } .workspace-filter.active,.workspace-icon:hover,.workspace-filter:hover { background:#f8f6f3; color:#241f2b; }
.workspace-ask { height:40px; padding:0 16px; display:inline-flex; align-items:center; gap:8px; border:1px solid #ffb09c; border-radius:11px; background:linear-gradient(180deg,#fff8f5 0%,#ffefe8 100%); color:#cf4f36; font-size:13px; font-weight:700; white-space:nowrap; box-shadow:0 2px 10px #ff6a5224, inset 0 1px 0 #ffffffaa; } .workspace-ask { height:40px; padding:0 var(--space-4); display:inline-flex; align-items:center; gap:var(--space-2); border:1px solid var(--agent-border); border-radius:var(--radius-sm); background:linear-gradient(180deg,#faf7ff 0%,#f3efff 100%); color:#6d4aff; font-size:var(--text-control); font-weight:700; white-space:nowrap; box-shadow:0 2px 10px #7c5cff1a, inset 0 1px 0 #ffffffaa; transition:background var(--motion-base) var(--ease-standard),box-shadow var(--motion-base) var(--ease-standard),transform var(--motion-fast) var(--ease-standard); }
.workspace-ask:hover { background:linear-gradient(180deg,#fff3ee 0%,#ffe8df 100%); box-shadow:0 3px 12px #ff6a5230; } .workspace-ask:hover { background:linear-gradient(180deg,#f5f0ff 0%,#ebe4ff 100%); box-shadow:0 3px 12px #7c5cff24; }
.workspace-account { margin-left:2px; padding:4px 8px 4px 4px; display:flex; align-items:center; gap:8px; border:0; border-radius:12px; background:transparent; color:#6d6872; } .agent-action { display:inline-flex; align-items:center; gap:var(--space-2); border:1px solid var(--agent-border); border-radius:var(--radius-sm); background:var(--agent-soft); color:#6d4aff; font-weight:600; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard),border-color var(--motion-fast) var(--ease-standard); }
.agent-action:hover { background:#ebe4ff; color:#5a3fe0; }
.agent-action.subtle { border-color:transparent; background:transparent; color:#7b67c8; font-weight:600; }
.agent-action.subtle:hover { background:var(--agent-soft); color:#5a3fe0; }
.agent-spark { display:inline-flex; align-items:center; justify-content:center; color:var(--agent); font-size:12px; line-height:1; font-weight:700; }
.agent-spark.pulse { animation:agentSparkPulse 1.2s ease-in-out infinite; }
.agent-label { display:inline-flex; align-items:center; gap:var(--space-1); }
.agent-insight { margin:0 0 var(--space-3); display:grid; gap:var(--space-2); }
.agent-insight-status { margin:0; display:flex; align-items:center; gap:var(--space-2); color:#6d4aff; font-size:var(--text-meta); font-weight:600; line-height:var(--line-meta); }
.agent-insight-block { padding:var(--space-3); border-radius:var(--radius-md); background:#fff; box-shadow:0 1px 2px #08082108; }
.agent-insight-block h4 { margin:0 0 var(--space-1); display:flex; align-items:center; gap:var(--space-1); color:#5a3fe0; font-size:var(--text-meta); font-weight:700; letter-spacing:.2px; text-transform:uppercase; }
.agent-insight-block p,.agent-insight-block li { margin:0; color:#4a454f; font-size:var(--text-content); line-height:var(--line-content); }
.agent-insight-block ul { margin:0; padding:0; list-style:none; display:grid; gap:var(--space-1); }
.agent-insight-actions li { display:flex; align-items:center; gap:var(--space-1); color:#6d4aff; font-size:var(--text-meta); font-weight:600; }
.agent-insight-block.reveal { animation:agentReveal var(--motion-slow) var(--ease-standard) both; }
.agent-insight-block.reveal.delay-1 { animation-delay:80ms; }
.agent-insight-block.reveal.delay-2 { animation-delay:160ms; }
@keyframes agentSparkPulse { 0%,100% { opacity:.55; transform:scale(.92); } 50% { opacity:1; transform:scale(1); } }
@keyframes agentReveal { from { opacity:0; transform:translateY(6px); } to { opacity:1; transform:translateY(0); } }
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes scaleIn { from { opacity:0; transform:translateY(8px) scale(.98); } to { opacity:1; transform:translateY(0) scale(1); } }
@keyframes slideInDetail { from { opacity:0; transform:translateX(12px); } to { opacity:1; transform:translateX(0); } }
.workspace-account { margin-left:2px; padding:var(--space-1) var(--space-2) var(--space-1) var(--space-1); display:flex; align-items:center; gap:var(--space-2); border:0; border-radius:var(--radius-lg); background:transparent; color:#6d6872; transition:background var(--motion-fast) var(--ease-standard); }
.workspace-account:hover { background:#f8f6f3; } .workspace-account:hover { background:#f8f6f3; }
.workspace-account>span { width:34px; height:34px; display:grid; place-items:center; border-radius:50%; background:#6d4aff; color:#fff; font-size:11px; font-weight:700; } .workspace-account>span { width:34px; height:34px; display:grid; place-items:center; border-radius:50%; background:#6d4aff; color:#fff; font-size:11px; font-weight:700; }
.workspace-account div { min-width:0; display:grid; text-align:left; } .workspace-account div { min-width:0; display:grid; text-align:left; }
.workspace-account strong,.workspace-account small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .workspace-account strong,.workspace-account small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.workspace-account strong { color:#241f2b; font-size:12px; } .workspace-account strong { color:#241f2b; font-size:12px; }
.workspace-account small { color:#8d8792; font-size:10px; max-width:150px; } .workspace-account small { color:#8d8792; font-size:10px; max-width:150px; }
.mailbox-stage { min-height:0; flex:1; display:grid; grid-template-columns:minmax(0,1fr) minmax(268px,286px); gap:16px; align-items:start; } .mailbox-stage { min-height:0; flex:1; display:grid; grid-template-columns:minmax(0,1fr) minmax(268px,286px); gap:var(--space-4); align-items:start; }
.mailbox-shell { min-height:0; max-height:calc(100vh - 110px); margin-top:0; overflow:hidden; border:1px solid #dbd6d0; border-radius:12px; background:#f0eeea; box-shadow:0 1px 2px #08082105; } .mailbox-shell { min-height:0; max-height:calc(100vh - 110px); margin-top:0; overflow:hidden; border:0; border-radius:var(--radius-xl); background:#f0eeea; box-shadow:var(--shadow-surface); }
.mailbox-list-pane { min-height:0; max-height:calc(100vh - 110px); display:flex; flex-direction:column; background:#f0eeea; border-radius:12px; } .mailbox-list-pane { min-height:0; max-height:calc(100vh - 110px); display:flex; flex-direction:column; background:#f0eeea; border-radius:var(--radius-xl); }
.mailbox-categories { min-height:42px; padding:4px 8px 0; display:flex; align-items:flex-end; gap:2px; border-bottom:1px solid #ddd8d2; background:#e8e5e0; overflow:auto; } .mailbox-categories { min-height:42px; padding:4px 8px 0; display:flex; align-items:flex-end; gap:2px; border-bottom:1px solid #ddd8d2; background:#e8e5e0; overflow:auto; }
.mailbox-categories button { height:38px; padding:0 10px 8px; position:relative; display:inline-flex; align-items:center; gap:6px; border:0; border-radius:8px 8px 0 0; background:transparent; color:#6b666f; font-size:12px; font-weight:600; white-space:nowrap; } .mailbox-categories button { height:40px; padding:0 var(--space-3) var(--space-2); position:relative; display:inline-flex; align-items:center; gap:var(--space-2); border:0; border-radius:var(--radius-sm) var(--radius-sm) 0 0; background:transparent; color:#6b666f; font-size:var(--text-meta); font-weight:600; white-space:nowrap; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
.mailbox-categories button svg { color:#8a858f; flex-shrink:0; } .mailbox-categories button svg { color:#8a858f; flex-shrink:0; }
.mailbox-category-label { line-height:1; } .mailbox-category-label { line-height:1; }
.mailbox-category-count { min-width:17px; height:17px; padding:0 5px; display:grid; place-items:center; border-radius:99px; background:#ff6a52; color:#fff; font-size:9px; font-weight:700; line-height:1; } .mailbox-category-count { min-width:17px; height:17px; padding:0 5px; display:grid; place-items:center; border-radius:99px; background:#ff6a52; color:#fff; font-size:9px; font-weight:700; line-height:1; }
@ -501,50 +526,65 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.mailbox-categories button.active svg { color:#e15b45; } .mailbox-categories button.active svg { color:#e15b45; }
.mailbox-categories button.active:after { content:""; position:absolute; left:8px; right:8px; bottom:0; height:3px; border-radius:3px 3px 0 0; background:linear-gradient(90deg,#ff8a72,#ff6a52); } .mailbox-categories button.active:after { content:""; position:absolute; left:8px; right:8px; bottom:0; height:3px; border-radius:3px 3px 0 0; background:linear-gradient(90deg,#ff8a72,#ff6a52); }
.mailbox-range { margin-left:auto; padding-right:8px; color:#9a948e; font-size:10px; font-weight:500; white-space:nowrap; } .mailbox-range { margin-left:auto; padding-right:8px; color:#9a948e; font-size:10px; font-weight:500; white-space:nowrap; }
.mailbox-selection-bar { min-height:42px; padding:6px 10px; display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:10px; border-bottom:1px solid #ddd8d2; background:#f3f1ed; }
.mailbox-selection-meta { display:flex; align-items:center; gap:8px; color:#4a4652; font-size:12px; font-weight:600; white-space:nowrap; }
.mailbox-selection-meta input { width:15px; height:15px; margin:0; accent-color:var(--coral); }
.mailbox-selection-actions { display:flex; flex-wrap:wrap; align-items:center; gap:4px; }
.mailbox-selection-actions button { height:32px; padding:0 var(--space-2); display:inline-flex; align-items:center; gap:var(--space-2); border:0; border-radius:var(--radius-sm); background:transparent; color:#5a5562; font-size:var(--text-meta); font-weight:600; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
.mailbox-selection-actions button:hover { background:#ebe8e3; color:#241f2b; }
.mailbox-selection-summarize { height:36px; padding:0 var(--space-3); white-space:nowrap; font-size:var(--text-meta); }
.mailbox-rows { flex:1; min-height:0; overflow:auto; background:#ebe8e3; } .mailbox-rows { flex:1; min-height:0; overflow:auto; background:#ebe8e3; }
.mailbox-row { width:100%; min-height:44px; padding:0 10px 0 8px; display:grid; grid-template-columns:15px 18px 30px minmax(0,1fr) auto; align-items:center; gap:7px; border:0; border-bottom:1px solid #00000007; background:transparent; color:#4a4652; text-align:left; cursor:pointer; transition:background .12s ease; } .mailbox-row { width:100%; min-height:40px; padding:0 var(--space-3) 0 var(--space-2); display:grid; grid-template-columns:16px 20px 32px minmax(0,1fr) minmax(120px,auto); align-items:center; gap:var(--space-2); position:relative; border:0; border-bottom:1px solid #00000007; background:transparent; color:#4a4652; text-align:left; cursor:pointer; transition:background var(--motion-fast) var(--ease-standard),box-shadow var(--motion-fast) var(--ease-standard); }
.mailbox-row.unread { background:#faf9f7; } .mailbox-row.unread { background:#faf9f7; }
.mailbox-row:not(.unread) { background:transparent; } .mailbox-row.unread .mailbox-unread-pip { opacity:1; }
.mailbox-row:hover { background:#e3e0db; } .mailbox-row:not(.unread) .mailbox-sender,.mailbox-row:not(.unread) .mailbox-subject { color:#5a5562; font-weight:500; }
.mailbox-row.selected { background:#fff1ec; box-shadow:inset 3px 0 0 #ff7a62; } .mailbox-row.unread .mailbox-sender,.mailbox-row.unread .mailbox-subject { color:#1a1628; font-weight:600; }
.mailbox-check { width:14px; height:14px; margin:0; accent-color:#ff6a52; } .mailbox-row:hover { z-index:1; background:#f5f3f0; box-shadow:var(--shadow-row); }
.mailbox-star { width:18px; height:18px; padding:0; display:grid; place-items:center; border:0; border-radius:6px; background:transparent; color:#c2bdc6; } .mailbox-row.checked { background:#f7f5ff; }
.mailbox-row.checked .mailbox-check { opacity:1; transform:scale(1); }
.mailbox-row.selected { background:#fff6f3; box-shadow:inset 2px 0 0 var(--coral),var(--shadow-row); }
.mailbox-row.checked.selected { background:#fff4f8; box-shadow:inset 2px 0 0 var(--coral),0 0 0 1px #7c5cff22 inset; }
.mailbox-row.checked .mailbox-sender { font-weight:600; }
.mailbox-check { width:16px; height:16px; margin:0; opacity:.72; accent-color:var(--coral); transition:opacity var(--motion-fast) var(--ease-standard),transform var(--motion-fast) var(--ease-standard); }
.mailbox-row:hover .mailbox-check,.mailbox-row.checked .mailbox-check,.mailbox-row:focus-within .mailbox-check { opacity:1; }
.mailbox-star { width:18px; height:18px; padding:0; display:grid; place-items:center; border:0; border-radius:var(--radius-xs); background:transparent; color:#c2bdc6; }
.mailbox-star:hover,.mailbox-star.active { color:#e8a317; background:#fff8e8; } .mailbox-star:hover,.mailbox-star.active { color:#e8a317; background:#fff8e8; }
.mailbox-row.starred .mailbox-star { color:#e8a317; } .mailbox-row.starred .mailbox-star { color:#e8a317; }
.sender-avatar.mailbox-avatar { width:30px; height:30px; font-size:10px; font-weight:700; } .sender-avatar.mailbox-avatar { width:32px; height:32px; font-size:var(--text-meta); font-weight:700; }
.sender-avatar-brand { background:#fff!important; border:1px solid #e8e4df; padding:4px; } .sender-avatar-brand { background:#fff!important; border:1px solid #e8e4df; padding:4px; }
.sender-avatar-brand img { object-fit:contain; } .sender-avatar-brand img { object-fit:contain; }
.mailbox-content { min-width:0; display:flex; align-items:center; gap:7px; overflow:hidden; } .mailbox-content { min-width:0; display:flex; align-items:center; gap:var(--space-2); overflow:hidden; }
.mailbox-unread-pip { width:6px; height:6px; flex-shrink:0; border-radius:50%; background:#4f8cff; box-shadow:0 0 0 2px #4f8cff22; } .mailbox-unread-pip { width:6px; height:6px; flex-shrink:0; border-radius:50%; background:#4f8cff; box-shadow:0 0 0 2px #4f8cff22; }
.mailbox-sender { flex:0 1 auto; max-width:28%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; } .mailbox-sender { flex:0 1 auto; max-width:28%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--text-content); line-height:var(--line-content); }
.mailbox-subject { flex:0 1 auto; max-width:30%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; } .mailbox-subject { flex:0 1 auto; max-width:30%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--text-content); line-height:var(--line-content); }
.mailbox-preview { flex:1 1 auto; min-width:48px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12.5px; font-weight:400; color:#9a959f; } .mailbox-preview { flex:1 1 auto; min-width:48px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:var(--text-meta); font-weight:400; line-height:var(--line-meta); color:#9a959f; }
.mailbox-preview-sep { margin-right:4px; color:#c2bdc6; } .mailbox-preview-sep { margin-right:var(--space-1); color:#c2bdc6; }
.mailbox-meta { flex:0 0 auto; display:inline-flex; align-items:center; gap:4px; } .mailbox-meta { flex:0 0 auto; display:inline-flex; align-items:center; gap:var(--space-1); }
.mailbox-row.unread .mailbox-sender,.mailbox-row.unread .mailbox-subject { color:#1a1628; font-weight:600; } .mailbox-time { color:#a39ea8; font-size:var(--text-meta); font-weight:400; line-height:var(--line-meta); white-space:nowrap; }
.mailbox-row:not(.unread) .mailbox-sender,.mailbox-row:not(.unread) .mailbox-subject { color:#5a5562; font-weight:500; } .mail-label,.mail-attachment { padding:var(--space-1) var(--space-2); border-radius:var(--radius-pill); font-size:var(--text-meta); font-style:normal; font-weight:600; white-space:nowrap; }
.mailbox-time { color:#a39ea8; font-size:10.5px; font-weight:400; white-space:nowrap; }
.mail-label,.mail-attachment { padding:1px 6px; border-radius:99px; font-size:9px; font-style:normal; font-weight:600; white-space:nowrap; }
.mail-label-rose { background:#ffe8e3; color:#c45a48; } .mail-label-rose { background:#ffe8e3; color:#c45a48; }
.mail-label-violet { background:#efe9ff; color:#6d54b8; } .mail-label-violet { background:#efe9ff; color:#6d54b8; }
.mail-label-blue { background:#e7f1ff; color:#3d6eae; } .mail-label-blue { background:#e7f1ff; color:#3d6eae; }
.mail-label-amber { background:#fff4d8; color:#9a7420; } .mail-label-amber { background:#fff4d8; color:#9a7420; }
.mail-label-green { background:#e7f6ee; color:#2f7a52; } .mail-label-green { background:#e7f6ee; color:#2f7a52; }
.mail-attachment { display:inline-flex; align-items:center; gap:2px; background:#e8e5e0; color:#7a7580; font-weight:500; } .mail-attachment { display:inline-flex; align-items:center; gap:2px; background:#e8e5e0; color:#7a7580; font-weight:500; }
.mailbox-hover-actions { display:none; align-items:center; gap:1px; } .mailbox-hover-actions { display:none; align-items:center; gap:2px; max-width:min(52vw,420px); overflow:hidden; }
.mailbox-row:hover .mailbox-hover-actions,.mailbox-row:focus-within .mailbox-hover-actions { display:flex; } .mailbox-row:hover .mailbox-hover-actions,.mailbox-row:focus-within .mailbox-hover-actions,.mailbox-row.selected .mailbox-hover-actions { display:flex; }
.mailbox-row:hover .mailbox-time,.mailbox-row:focus-within .mailbox-time { display:none; } .mailbox-row:hover .mailbox-time,.mailbox-row:focus-within .mailbox-time,.mailbox-row.selected .mailbox-time { display:none; }
.mailbox-hover-actions button { width:26px; height:26px; display:grid; place-items:center; border:0; border-radius:7px; background:#f8f6f3; color:#6d6872; box-shadow:0 1px 2px #0808210c; } .mailbox-hover-actions button { height:32px; min-width:32px; padding:0 var(--space-2); display:inline-flex; align-items:center; gap:var(--space-1); border:0; border-radius:var(--radius-xs); background:transparent; color:#6d6872; font-size:var(--text-meta); font-weight:600; white-space:nowrap; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
.mailbox-hover-actions button:hover { color:#e15b45; background:#fff4f1; } .mailbox-hover-actions button:hover { color:#241f2b; background:#ffffffb8; }
.mailbox-rail { min-width:0; display:flex; flex-direction:column; gap:12px; max-height:calc(100vh - 96px); position:sticky; top:14px; } .mailbox-hover-actions button.agent-action { border:1px solid var(--agent-border); background:var(--agent-soft); color:#6d4aff; }
.agent-panel { padding:16px; border:1px solid #e4e0da; border-radius:12px; background:#faf9f7; box-shadow:0 1px 2px #08082105; } .mailbox-hover-actions button.agent-action:hover { background:#ebe4ff; color:#5a3fe0; }
.agent-panel-head { display:flex; align-items:center; gap:10px; } .mailbox-hover-sep { color:#c8c3cc; font-size:10px; user-select:none; }
.agent-panel-mark { width:32px; height:32px; display:grid; place-items:center; border-radius:10px; background:#fff1eb; color:#e15b45; } .mailbox-rail { min-width:0; display:flex; flex-direction:column; gap:var(--space-3); max-height:calc(100vh - 96px); position:sticky; top:var(--space-3); }
.agent-panel-head h2 { margin:0; font:700 15px "Manrope"; letter-spacing:-.3px; } .agent-panel { padding:var(--space-4); border:0; border-radius:var(--radius-lg); background:#faf9f7; box-shadow:var(--shadow-surface); transition:box-shadow var(--motion-base) var(--ease-standard); }
.agent-panel-head p { margin:1px 0 0; color:#8d8792; font-size:11px; } .agent-panel-head { display:flex; align-items:center; gap:var(--space-3); }
.agent-panel-hello { margin:14px 0 12px; display:grid; gap:3px; } .agent-panel-mark { width:36px; height:36px; display:grid; place-items:center; border-radius:var(--radius-md); background:var(--agent-soft); color:var(--agent); font-size:16px; }
.agent-panel-hello strong { font-size:15px; letter-spacing:-.2px; } .agent-panel-head h2 { margin:0; font:700 16px "Manrope"; letter-spacing:-.3px; }
.agent-panel-hello span,.agent-panel-hello p { margin:0; color:#7a746e; font-size:12px; line-height:1.45; } .agent-panel-head p { margin:var(--space-1) 0 0; color:#8d8792; font-size:var(--text-meta); line-height:var(--line-meta); }
.agent-panel-hello { margin:var(--space-4) 0 var(--space-3); display:grid; gap:var(--space-1); }
.agent-panel-hello strong { font-size:var(--text-content); letter-spacing:-.2px; line-height:var(--line-content); }
.agent-panel-hello span,.agent-panel-hello p { margin:0; color:#7a746e; font-size:var(--text-meta); line-height:var(--line-content); }
.agent-stat-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; } .agent-stat-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.agent-stat { min-height:92px; padding:10px; display:grid; align-content:start; gap:2px; border:0; border-radius:12px; text-align:left; } .agent-stat { min-height:92px; padding:10px; display:grid; align-content:start; gap:2px; border:0; border-radius:12px; text-align:left; }
.agent-stat strong { font:700 20px "Manrope"; letter-spacing:-.6px; color:#241f2b; } .agent-stat strong { font:700 20px "Manrope"; letter-spacing:-.6px; color:#241f2b; }
@ -557,13 +597,61 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.agent-quick { margin-top:14px; } .agent-quick { margin-top:14px; }
.agent-quick h3 { margin:0 0 8px; color:#8d8792; font-size:11px; letter-spacing:.4px; text-transform:uppercase; } .agent-quick h3 { margin:0 0 8px; color:#8d8792; font-size:11px; letter-spacing:.4px; text-transform:uppercase; }
.agent-quick div { display:grid; grid-template-columns:1fr 1fr; gap:8px; } .agent-quick div { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.agent-quick button { min-height:36px; padding:0 8px; display:flex; align-items:center; justify-content:center; gap:6px; border:1px solid #eee8e3; border-radius:10px; background:#fff; color:#4d4852; font-size:11px; font-weight:600; } .agent-quick button { min-height:36px; padding:0 var(--space-2); display:flex; align-items:center; justify-content:center; gap:var(--space-2); border:0; border-radius:var(--radius-sm); background:#fff; color:#4d4852; font-size:var(--text-meta); font-weight:600; box-shadow:0 1px 2px #08082108; transition:background var(--motion-fast) var(--ease-standard),color var(--motion-fast) var(--ease-standard); }
.agent-quick button:hover { border-color:#ffd3c8; color:#e15b45; background:#fff8f5; } .agent-quick button:hover { color:#241f2b; background:#f8f6f3; }
.mailbox-notes-pane.compact { min-height:0; max-height:240px; border-radius:12px; border-color:#e4e0da; box-shadow:0 1px 2px #08082105; } .agent-quick button.agent-action.subtle { box-shadow:none; }
.mailbox-notes-pane.compact .sticky-notes-header { min-height:44px; padding:8px 12px; } .agent-quick button.agent-action.subtle:hover { background:var(--agent-soft); color:#5a3fe0; }
.mailbox-notes-pane.compact .sticky-notes-body { padding:10px; gap:8px; } .agent-caught-up { margin:0 0 4px; padding:12px; display:flex; flex-wrap:wrap; align-items:center; gap:8px; border-radius:10px; background:#f3f1ee; color:#6f6a64; font-size:12px; }
.agent-caught-up p { margin:0; flex:1; min-width:120px; }
.agent-mini-metric { padding:4px 10px; border:1px solid #e4e0da; border-radius:999px; background:#fff; color:#5c5763; font-size:11px; font-weight:600; }
.agent-mini-metric:hover { border-color:#ffd3c8; color:#e15b45; }
.agent-context-block { margin-top:14px; padding-top:12px; border-top:1px solid #ece7e1; }
.agent-context-block h3 { margin:0 0 8px; display:flex; align-items:center; gap:5px; color:#8d8792; font-size:11px; letter-spacing:.4px; text-transform:uppercase; }
.agent-context-block ul { margin:0; padding:0; list-style:none; display:grid; gap:6px; }
.agent-context-block li { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size:12px; color:#4a454f; }
.agent-context-block time { min-width:42px; color:#e15b45; font-size:11px; font-weight:700; }
.agent-context-block em { color:#8d8792; font-size:11px; font-style:normal; font-weight:600; }
.agent-context-block button { width:100%; padding:6px 0; display:flex; align-items:center; justify-content:space-between; gap:8px; border:0; background:transparent; color:inherit; font:inherit; text-align:left; cursor:pointer; }
.agent-context-block button:hover span { color:#e15b45; }
.command-palette-overlay { position:fixed; inset:0; z-index:120; display:grid; place-items:start center; padding:12vh var(--space-4) var(--space-4); background:#120f1a66; backdrop-filter:blur(4px); animation:fadeIn var(--motion-base) var(--ease-standard); }
.command-palette { width:min(560px,100%); border:0; border-radius:var(--radius-float); background:#faf9f7; box-shadow:0 24px 60px #08082133; overflow:hidden; animation:scaleIn var(--motion-slow) var(--ease-standard); }
.command-palette-input { display:flex; align-items:center; gap:10px; padding:14px 16px; border-bottom:1px solid #ece7e1; }
.command-palette-input input { flex:1; border:0; background:transparent; color:#2a2530; font:500 15px "DM Sans"; outline:none; }
.command-palette-input input::placeholder { color:#9a949e; }
.command-palette-input svg { color:#9a949e; flex-shrink:0; }
.command-palette-input kbd { padding:2px 6px; border:1px solid #e4e0da; border-radius:6px; background:#fff; color:#8d8792; font-size:10px; }
.command-palette-list { margin:0; padding:6px; list-style:none; max-height:320px; overflow:auto; }
.command-palette-list button { width:100%; padding:10px 12px; display:flex; align-items:flex-start; gap:10px; border:0; border-radius:10px; background:transparent; color:#3d3944; text-align:left; cursor:pointer; }
.command-palette-list button strong { display:block; font-size:13px; font-weight:600; }
.command-palette-list button small { display:block; margin-top:1px; color:#8d8792; font-size:11px; font-weight:400; }
.command-palette-list button.active,.command-palette-list button:hover { background:#fff1eb; color:#c24f3a; }
.command-palette-list button.active small,.command-palette-list button:hover small { color:#b76f60; }
.command-palette-empty { padding:18px 12px; color:#8d8792; font-size:12px; text-align:center; }
.command-palette-foot { padding:10px 14px; display:flex; flex-wrap:wrap; gap:12px; border-top:1px solid #ece7e1; color:#9a949e; font-size:10px; }
.command-palette-foot kbd { margin-right:4px; padding:1px 5px; border:1px solid #e4e0da; border-radius:5px; background:#fff; }
.workspace-search-kbd { padding:0; border:0; background:transparent; cursor:pointer; }
.workspace-search-kbd kbd { pointer-events:none; }
.mail-detail-agent-bar { margin-bottom:16px; display:flex; flex-wrap:wrap; gap:6px; }
.mail-detail.open { animation:slideInDetail var(--motion-slow) var(--ease-standard); }
.mail-detail-agent-bar .agent-action.subtle { height:36px; padding:0 var(--space-3); font-size:var(--text-meta); border-radius:var(--radius-sm); }
.mail-detail-content h2 { margin-top:0; }
.compose-agent-mark { width:34px; height:34px; display:grid; place-items:center; border-radius:var(--radius-md); background:var(--agent-soft); color:var(--agent); }
.compose-ai .agent-action { height:36px; padding:0 12px; font-size:12px; white-space:nowrap; }
.mailbox-notes-pane.compact { min-height:0; border-radius:var(--radius-lg); border:0; background:#faf9f7; box-shadow:0 1px 2px #08082108; overflow:hidden; }
.mailbox-notes-pane.compact.expanded { max-height:min(42vh,320px); display:flex; flex-direction:column; }
.mailbox-notes-pane.compact.expanded .sticky-notes-body { animation:agentReveal var(--motion-slow) var(--ease-standard); }
.sticky-notes-toggle { width:100%; min-height:40px; padding:0 12px; display:flex; align-items:center; gap:8px; border:0; background:transparent; color:#5c5763; font:600 12px "DM Sans"; cursor:pointer; }
.sticky-notes-toggle svg:first-child { transform:rotate(-24deg); color:#bd5947; }
.sticky-notes-toggle span { flex:1; text-align:left; }
.sticky-notes-toggle span small { color:#8d8792; font-weight:500; }
.sticky-notes-toggle .open { transform:rotate(180deg); }
.mailbox-notes-pane.compact .sticky-notes-body { flex:1; padding:0 10px 10px; display:flex; flex-direction:column; gap:8px; overflow:auto; }
.sticky-notes-context { margin:0; padding:0 2px; color:#8d8792; font-size:11px; }
.sticky-notes-toolbar { display:flex; justify-content:flex-end; }
.sticky-notes-toolbar button { width:30px; height:30px; padding:0; display:grid; place-items:center; border:0; border-radius:8px; background:#fff; color:#77717a; box-shadow:0 1px 2px #0808210c; cursor:pointer; }
.sticky-notes-toolbar button:hover { color:#bd5947; background:#fff0eb; }
.mailbox-notes-pane.compact .sticky-note { height:92px; min-height:92px; padding:12px 12px 8px; } .mailbox-notes-pane.compact .sticky-note { height:92px; min-height:92px; padding:12px 12px 8px; }
.mailbox-notes-pane.compact .sticky-note-add { min-height:64px; } .mailbox-notes-pane.compact .sticky-note-add { min-height:48px; }
@media(max-width:1100px) { @media(max-width:1100px) {
.workspace-header { grid-template-columns:1fr; gap:10px; } .workspace-header { grid-template-columns:1fr; gap:10px; }
.workspace-zone-actions { padding:0; border:0; } .workspace-zone-actions { padding:0; border:0; }
@ -580,6 +668,8 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.mailbox-sender,.mailbox-subject { max-width:46%; } .mailbox-sender,.mailbox-subject { max-width:46%; }
.mailbox-preview { flex-basis:100%; max-width:100%; } .mailbox-preview { flex-basis:100%; max-width:100%; }
.mailbox-hover-actions { display:none!important; } .mailbox-hover-actions { display:none!important; }
.mailbox-row:hover .mailbox-time { display:block; } .mailbox-row:hover .mailbox-time,.mailbox-row.selected .mailbox-time { display:block; }
.mailbox-selection-bar { grid-template-columns:1fr; }
.mailbox-selection-summarize { justify-self:start; }
} }
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto!important; transition:none!important; } } @media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto!important; transition:none!important; } }

View file

@ -1,3 +1,5 @@
import { senderDisplayName } from "./senderAvatar";
const CATEGORY_ORDER = ["Primary", "Updates", "Promotions", "Social", "Finance"]; const CATEGORY_ORDER = ["Primary", "Updates", "Promotions", "Social", "Finance"];
export function emailSearchText(email) { export function emailSearchText(email) {
@ -88,6 +90,95 @@ export function attachmentCount(email) {
return Number(email.attachment_count || 0); return Number(email.attachment_count || 0);
} }
function emailTimestamp(email) {
const value = email.received_at || email.sent_at || email.created_at || email.updated_at;
if (!value) return null;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
function formatRelativeAge(date) {
if (!date) return "";
const hours = Math.floor((Date.now() - date.getTime()) / 3600000);
if (hours < 1) return "<1h";
if (hours < 24) return `${hours}h`;
return `${Math.floor(hours / 24)}d`;
}
function extractMeetingTime(text) {
const match = text.match(/\b(\d{1,2}:\d{2})\s*(am|pm)?/i)
|| text.match(/\b(\d{1,2})\s*(am|pm)\b/i);
if (!match) return null;
return match[0].replace(/\s+/g, "").toUpperCase();
}
function extractDeadline(text) {
if (/tomorrow/i.test(text)) return "Tomorrow";
const weekday = text.match(/\b(monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/i);
if (weekday) {
return weekday[1].charAt(0).toUpperCase() + weekday[1].slice(1);
}
if (/today/i.test(text)) return "Today";
return null;
}
export function inboxContext(emails) {
const stats = inboxIntelligence(emails);
const needsReply = emails
.filter((email) =>
/please|reply|respond|confirm|review|attached|need your|waiting|action required/.test(
emailSearchText(email),
),
)
.sort((left, right) => {
const leftDate = emailTimestamp(left)?.getTime() || 0;
const rightDate = emailTimestamp(right)?.getTime() || 0;
return rightDate - leftDate;
})
.slice(0, 4)
.map((email) => ({
id: email.id,
name: senderDisplayName(email),
age: formatRelativeAge(emailTimestamp(email)),
}));
const today = [];
emails.forEach((email) => {
const text = emailSearchText(email);
if (!/meet|meeting|call|interview|sync|calendar/.test(text)) return;
const time = extractMeetingTime(text);
if (!time) return;
const label = (email.subject || "Meeting").trim().slice(0, 42);
if (!today.some((item) => item.time === time && item.label === label)) {
today.push({ time, label });
}
});
const deadlines = [];
emails.forEach((email) => {
const text = emailSearchText(email);
if (!/invoice|proposal|payment|due|deadline|bill/.test(text)) return;
const due = extractDeadline(text);
if (!due) return;
const label = (email.subject || "Deadline")
.split(/[-–:|]/)
.map((part) => part.trim())
.find(Boolean)
?.slice(0, 28) || "Deadline";
if (!deadlines.some((item) => item.label === label && item.due === due)) {
deadlines.push({ label, due });
}
});
return {
stats,
needsReply,
today: today.slice(0, 4),
deadlines: deadlines.slice(0, 4),
};
}
export function labelCounts(emails) { export function labelCounts(emails) {
const counts = { const counts = {
Important: 0, Important: 0,