feat: inline search filters, folder icon picker, richer demo data & UI polish

- Replace advanced search panel with inline filter chips and collapsible fields
  (from/to/subject/body inputs, folder dropdown, attachment/flagged/read toggles)
- Add debounced auto-search on filter field changes
- Folder settings: add icon picker for custom per-folder icons, toast feedback,
  reorder sections (folder list first, roles second), empty-state illustration
- Avatar: show sender favicon for company domains, randomuser.me portraits for
  personal emails, custom avatars for demo senders, fallback to initials
- Add /api/favicon proxy route for fetching domain favicons
- Settings store: add senderFavicons toggle and folderIcons persistence
- Richer mock inbox with 11 realistic emails (GitHub, Slack, Stripe, Vercel, etc.)
- Navigation rail & sidebar cleanup, permanent-delete warning i18n (all 8 locales)
- Rename search toggle label to 'More', add body/folder filter translations
This commit is contained in:
Linus Rath
2026-03-09 20:59:32 +01:00
parent d41718768b
commit 8985865843
21 changed files with 950 additions and 341 deletions
+102 -24
View File
@@ -1253,11 +1253,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
keywords: { $seen: false },
size: 1024,
receivedAt: new Date().toISOString(),
from: [{ name: "Alice Johnson", email: "alice@example.com" }],
from: [{ name: "GitHub", email: "notifications@github.com" }],
to: [{ email: "you@example.com" }],
subject: "Q4 Budget Review Meeting",
preview: "Hi team, I wanted to schedule a meeting to review our Q4 budget projections. Are you available this Thursday at 2 PM? We need to discuss...",
hasAttachment: true,
subject: "[jmap-webmail] New pull request #42: Add OAuth2 module",
preview: "dependabot[bot] opened a pull request in root-fr/jmap-webmail. This PR adds a comprehensive authentication module with OAuth2 PKCE support...",
hasAttachment: false,
},
{
id: "2",
@@ -1266,11 +1266,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
keywords: { $seen: true, $flagged: true },
size: 512,
receivedAt: new Date(Date.now() - 3600000).toISOString(),
from: [{ name: "Bob Smith", email: "bob@company.com" }],
from: [{ name: "Emily Chen", email: "emily.chen@gmail.com" }],
to: [{ email: "you@example.com" }],
subject: "Re: Project Timeline Update",
preview: "Thanks for the update. The new timeline looks good to me. I've reviewed the milestones and everything seems achievable...",
hasAttachment: false,
subject: "Re: Dashboard Redesign v2 — feedback",
preview: "Hey! I just pushed the updated mockups to Figma. I incorporated all the feedback from last week's meeting. Let me know what you think about the new nav...",
hasAttachment: true,
},
{
id: "3",
@@ -1279,11 +1279,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
keywords: { $seen: false },
size: 2048,
receivedAt: new Date(Date.now() - 7200000).toISOString(),
from: [{ name: "Carol White", email: "carol@design.co" }],
from: [{ name: "Slack", email: "notifications@slack.com" }],
to: [{ email: "you@example.com" }],
subject: "New Design Mockups Ready",
preview: "Hey! The new mockups for the landing page are ready for review. I've incorporated all the feedback from last week's meeting...",
hasAttachment: true,
subject: "3 new messages in #engineering",
preview: "Marcus: Hey team, the CI pipeline is green again. Sarah: Great, merging the feature branch now. Alex: Let's do a quick sync at 3 PM...",
hasAttachment: false,
},
{
id: "4",
@@ -1291,11 +1291,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 768,
receivedAt: new Date(Date.now() - 86400000).toISOString(),
from: [{ name: "GitHub", email: "notifications@github.com" }],
receivedAt: new Date(Date.now() - 14400000).toISOString(),
from: [{ name: "Marcus Rivera", email: "marcus.rivera@outlook.com" }],
to: [{ email: "you@example.com" }],
subject: "[PR] Feature: Add authentication module",
preview: "A new pull request has been opened in your repository. This PR adds a comprehensive authentication module with OAuth support...",
subject: "Quick question about the API rate limits",
preview: "Hey, I was looking at the JMAP spec and I'm not sure how we should handle rate limiting on the server side. Do you have any thoughts on...",
hasAttachment: false,
},
{
@@ -1304,13 +1304,91 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 1536,
receivedAt: new Date(Date.now() - 172800000).toISOString(),
from: [{ name: "David Lee", email: "david@startup.io" }],
receivedAt: new Date(Date.now() - 86400000).toISOString(),
from: [{ name: "Stripe", email: "receipts@stripe.com" }],
to: [{ email: "you@example.com" }],
subject: "Investment Proposal Discussion",
preview: "Following up on our call yesterday, I'm sending over the investment proposal we discussed. The terms are quite favorable...",
subject: "Your invoice from Acme Corp is ready",
preview: "Invoice #INV-2026-0312 for $49.00 has been paid. Thank you for your payment. View your receipt and download your invoice...",
hasAttachment: true,
},
{
id: "6",
threadId: "thread-6",
mailboxIds: { inbox: true },
keywords: { $seen: false },
size: 3072,
receivedAt: new Date(Date.now() - 108000000).toISOString(),
from: [{ name: "Sarah Kim", email: "sarah.kim@proton.me" }],
to: [{ email: "you@example.com" }],
subject: "Conference talk proposal — need your review",
preview: "I'm submitting a talk to ReactConf about our email client architecture. Could you take a look at my abstract before the deadline on Friday?...",
hasAttachment: true,
},
{
id: "7",
threadId: "thread-7",
mailboxIds: { inbox: true },
keywords: { $seen: true, $flagged: true },
size: 4096,
receivedAt: new Date(Date.now() - 172800000).toISOString(),
from: [{ name: "Vercel", email: "notifications@vercel.com" }],
to: [{ email: "you@example.com" }],
subject: "Deployment successful: jmap-webmail \u2192 Production",
preview: "Your project jmap-webmail has been deployed to production. Build completed in 47s. All checks passed. Preview: https://jmap-webmail.vercel.app...",
hasAttachment: false,
},
{
id: "8",
threadId: "thread-8",
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 2560,
receivedAt: new Date(Date.now() - 259200000).toISOString(),
from: [{ name: "Alex Petrov", email: "alex.petrov@fastmail.com" }],
to: [{ email: "you@example.com" }],
subject: "Meeting notes from yesterday's standup",
preview: "Here are the action items from yesterday. 1) Finish the drag-and-drop implementation by Wednesday. 2) Review the accessibility audit results...",
hasAttachment: false,
},
{
id: "9",
threadId: "thread-9",
mailboxIds: { inbox: true },
keywords: { $seen: false },
size: 1280,
receivedAt: new Date(Date.now() - 345600000).toISOString(),
from: [{ name: "Linear", email: "notifications@linear.app" }],
to: [{ email: "you@example.com" }],
subject: "ENG-384: Implement email threading view \u2014 moved to In Progress",
preview: "Alice Johnson moved ENG-384 to In Progress. This issue covers implementing the conversation thread view for the email client...",
hasAttachment: false,
},
{
id: "10",
threadId: "thread-10",
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 896,
receivedAt: new Date(Date.now() - 432000000).toISOString(),
from: [{ name: "Priya Sharma", email: "priya.sharma@icloud.com" }],
to: [{ email: "you@example.com" }],
subject: "Re: Onboarding docs for new contributors",
preview: "Thanks for putting this together! I added a section on setting up the dev environment. Also linked the architecture diagram from our wiki...",
hasAttachment: false,
},
{
id: "11",
threadId: "thread-11",
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 5120,
receivedAt: new Date(Date.now() - 518400000).toISOString(),
from: [{ name: "LaunchWeekly", email: "newsletter@launchweekly.com" }],
to: [{ email: "you@example.com" }],
subject: "\uD83D\uDE80 This week in tech: AI agents, new frameworks, and more",
preview: "Happy Monday! Here's your weekly roundup of the most interesting launches, open-source projects, and developer tools you might have missed...",
hasAttachment: false,
},
];
const mockMailboxes: Mailbox[] = [
@@ -1319,10 +1397,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
name: "Inbox",
role: "inbox",
sortOrder: 1,
totalEmails: 5,
unreadEmails: 2,
totalThreads: 5,
unreadThreads: 2,
totalEmails: 11,
unreadEmails: 4,
totalThreads: 11,
unreadThreads: 4,
myRights: {
mayReadItems: true,
mayAddItems: true,
+28
View File
@@ -41,6 +41,12 @@ interface SettingsState {
calendarNotificationsEnabled: boolean;
calendarNotificationSound: boolean;
// Experimental
senderFavicons: boolean;
// Folders
folderIcons: Record<string, string>; // mailboxId -> icon name
// Advanced
debugMode: boolean;
@@ -53,6 +59,10 @@ interface SettingsState {
exportSettings: () => string;
importSettings: (json: string) => boolean;
// Folder icons
setFolderIcon: (mailboxId: string, icon: string) => void;
removeFolderIcon: (mailboxId: string) => void;
// Trusted senders
addTrustedSender: (email: string) => void;
removeTrustedSender: (email: string) => void;
@@ -90,6 +100,12 @@ const DEFAULT_SETTINGS = {
calendarNotificationsEnabled: true,
calendarNotificationSound: true,
// Experimental
senderFavicons: true,
// Folders
folderIcons: {} as Record<string, string>,
// Advanced
debugMode: false,
};
@@ -146,6 +162,8 @@ export const useSettingsStore = create<SettingsState>()(
sessionTimeout: state.sessionTimeout,
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
calendarNotificationSound: state.calendarNotificationSound,
senderFavicons: state.senderFavicons,
folderIcons: state.folderIcons,
debugMode: state.debugMode,
};
return JSON.stringify(settings, null, 2);
@@ -179,6 +197,16 @@ export const useSettingsStore = create<SettingsState>()(
}
},
// Folder icon methods
setFolderIcon: (mailboxId: string, icon: string) => {
set({ folderIcons: { ...get().folderIcons, [mailboxId]: icon } });
},
removeFolderIcon: (mailboxId: string) => {
const { [mailboxId]: _, ...rest } = get().folderIcons;
set({ folderIcons: rest });
},
// Trusted senders methods
addTrustedSender: (email: string) => {
const normalizedEmail = email.toLowerCase().trim();