UI enhancement
This commit is contained in:
parent
45e3898acb
commit
23239c5cb6
5 changed files with 225 additions and 129 deletions
|
|
@ -89,26 +89,34 @@ export default function MailRow({
|
|||
aria-label={starred ? "Unstar message" : "Star message"}
|
||||
aria-pressed={starred}
|
||||
>
|
||||
<Star size={15} fill={starred ? "currentColor" : "none"} />
|
||||
<Star size={14} fill={starred ? "currentColor" : "none"} />
|
||||
</button>
|
||||
<SenderAvatar email={email} className="mailbox-avatar" />
|
||||
<span className="mailbox-sender">
|
||||
<div className="mailbox-content">
|
||||
{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 className="mailbox-sender">{senderName}</span>
|
||||
<span className="mailbox-subject">{subject}</span>
|
||||
{snippet && (
|
||||
<span className="mailbox-preview">
|
||||
<span className="mailbox-preview-sep" aria-hidden="true">—</span>
|
||||
{snippet}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<time dateTime={timestamp || undefined}>{formatTimestamp(timestamp)}</time>
|
||||
{(labels.length > 0 || files > 0) && (
|
||||
<span className="mailbox-meta">
|
||||
{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={11} />
|
||||
{files > 1 ? files : ""}
|
||||
</em>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<time className="mailbox-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>
|
||||
|
|
|
|||
|
|
@ -6,10 +6,16 @@ import {
|
|||
CircleHelp,
|
||||
Filter,
|
||||
Inbox,
|
||||
LayoutGrid,
|
||||
Mail,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Sun,
|
||||
Tag,
|
||||
Users,
|
||||
Wallet,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
|
|
@ -34,7 +40,14 @@ import {
|
|||
} from "../utils/inboxSignals";
|
||||
|
||||
const PAGE_SIZE = 25;
|
||||
const CATEGORIES = ["Primary", "Updates", "Promotions", "Social", "Finance", "All Mail"];
|
||||
const CATEGORY_TABS = [
|
||||
{ name: "Primary", icon: LayoutGrid },
|
||||
{ name: "Updates", icon: Bell },
|
||||
{ name: "Promotions", icon: Tag },
|
||||
{ name: "Social", icon: Users },
|
||||
{ name: "Finance", icon: Wallet },
|
||||
{ name: "All Mail", icon: Mail },
|
||||
];
|
||||
|
||||
function resultPage(result) {
|
||||
if (Array.isArray(result)) return { items: result, nextCursor: null };
|
||||
|
|
@ -343,41 +356,47 @@ export default function MailboxView({
|
|||
return (
|
||||
<section className="mailbox-workspace">
|
||||
<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>
|
||||
<div className="workspace-zone workspace-zone-command">
|
||||
<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>
|
||||
</div>
|
||||
<div className="workspace-zone workspace-zone-actions">
|
||||
<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>
|
||||
</div>
|
||||
<div className="workspace-zone workspace-zone-account">
|
||||
<button type="button" className="workspace-icon" aria-label="Refresh" onClick={load}><RefreshCw size={16} /></button>
|
||||
<button type="button" className="workspace-icon" aria-label="Theme"><Sun 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>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="mailbox-stage">
|
||||
|
|
@ -386,7 +405,7 @@ export default function MailboxView({
|
|||
<div className="mailbox-list-pane">
|
||||
{mailbox === "inbox" && (
|
||||
<div className="mailbox-categories" role="tablist" aria-label="Inbox categories">
|
||||
{CATEGORIES.map((name) => (
|
||||
{CATEGORY_TABS.map(({ name, icon: Icon }) => (
|
||||
<button
|
||||
key={name}
|
||||
type="button"
|
||||
|
|
@ -395,8 +414,13 @@ export default function MailboxView({
|
|||
className={category === name ? "active" : ""}
|
||||
onClick={() => setCategory(name)}
|
||||
>
|
||||
{name}
|
||||
{name !== "All Mail" && counts[name] > 0 && <span>{counts[name]}</span>}
|
||||
<Icon size={14} strokeWidth={1.75} />
|
||||
<span className="mailbox-category-label">{name}</span>
|
||||
{name !== "All Mail" && (
|
||||
<span className={`mailbox-category-count${counts[name] > 0 ? "" : " muted"}`}>
|
||||
{counts[name] || 0}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
<small className="mailbox-range">
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { useState } from "react";
|
||||
import { senderAvatar } from "../utils/senderAvatar";
|
||||
|
||||
export default function SenderAvatar({ email, className = "" }) {
|
||||
const { name, initials, color, imageUrl } = senderAvatar(email);
|
||||
const { initials, color, imageUrl, brandUrl, isBrand } = senderAvatar(email);
|
||||
const [broken, setBroken] = useState(false);
|
||||
const src = imageUrl || brandUrl;
|
||||
|
||||
if (imageUrl) {
|
||||
if (src && !broken) {
|
||||
return (
|
||||
<span className={`sender-avatar${className ? ` ${className}` : ""}`}>
|
||||
<img src={imageUrl} alt="" aria-hidden="true" />
|
||||
<span
|
||||
className={`sender-avatar${isBrand ? " sender-avatar-brand" : ""}${className ? ` ${className}` : ""}`}
|
||||
>
|
||||
<img src={src} alt="" aria-hidden="true" onError={() => setBroken(true)} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue