UI enhancement

This commit is contained in:
debu-2 2026-10-03 22:28:20 +05:30
parent 45e3898acb
commit 23239c5cb6
5 changed files with 225 additions and 129 deletions

View file

@ -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>

View file

@ -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">

View file

@ -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>
);
}