- 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
281 lines
7.9 KiB
TypeScript
281 lines
7.9 KiB
TypeScript
import { create } from 'zustand';
|
|
import { persist } from 'zustand/middleware';
|
|
|
|
export type FontSize = 'small' | 'medium' | 'large';
|
|
export type ListDensity = 'compact' | 'regular' | 'comfortable';
|
|
export type DeleteAction = 'trash' | 'permanent';
|
|
export type ReplyMode = 'reply' | 'replyAll';
|
|
export type DateFormat = 'regional' | 'iso' | 'custom';
|
|
export type TimeFormat = '12h' | '24h';
|
|
export type FirstDayOfWeek = 0 | 1; // 0 = Sunday, 1 = Monday
|
|
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
|
|
|
interface SettingsState {
|
|
// Appearance
|
|
fontSize: FontSize;
|
|
listDensity: ListDensity;
|
|
animationsEnabled: boolean;
|
|
|
|
// Language & Region
|
|
dateFormat: DateFormat;
|
|
timeFormat: TimeFormat;
|
|
firstDayOfWeek: FirstDayOfWeek;
|
|
|
|
// Email Behavior
|
|
markAsReadDelay: number; // milliseconds (0 = instant, -1 = never)
|
|
deleteAction: DeleteAction;
|
|
showPreview: boolean;
|
|
emailsPerPage: number;
|
|
externalContentPolicy: ExternalContentPolicy;
|
|
|
|
// Composer
|
|
autoSaveDraftInterval: number; // milliseconds
|
|
sendConfirmation: boolean;
|
|
defaultReplyMode: ReplyMode;
|
|
|
|
// Privacy & Security
|
|
sessionTimeout: number; // minutes (0 = never)
|
|
trustedSenders: string[]; // Email addresses that can load external content
|
|
|
|
// Calendar Notifications
|
|
calendarNotificationsEnabled: boolean;
|
|
calendarNotificationSound: boolean;
|
|
|
|
// Experimental
|
|
senderFavicons: boolean;
|
|
|
|
// Folders
|
|
folderIcons: Record<string, string>; // mailboxId -> icon name
|
|
|
|
// Advanced
|
|
debugMode: boolean;
|
|
|
|
// Actions
|
|
updateSetting: <K extends keyof SettingsState>(
|
|
key: K,
|
|
value: SettingsState[K]
|
|
) => void;
|
|
resetToDefaults: () => void;
|
|
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;
|
|
isSenderTrusted: (email: string) => boolean;
|
|
}
|
|
|
|
const DEFAULT_SETTINGS = {
|
|
// Appearance
|
|
fontSize: 'medium' as FontSize,
|
|
listDensity: 'regular' as ListDensity,
|
|
animationsEnabled: true,
|
|
|
|
// Language & Region
|
|
dateFormat: 'regional' as DateFormat,
|
|
timeFormat: '24h' as TimeFormat,
|
|
firstDayOfWeek: 1 as FirstDayOfWeek, // Monday
|
|
|
|
// Email Behavior
|
|
markAsReadDelay: 0, // Instant
|
|
deleteAction: 'trash' as DeleteAction,
|
|
showPreview: true,
|
|
emailsPerPage: 50,
|
|
externalContentPolicy: 'ask' as ExternalContentPolicy,
|
|
|
|
// Composer
|
|
autoSaveDraftInterval: 60000, // 1 minute
|
|
sendConfirmation: false,
|
|
defaultReplyMode: 'reply' as ReplyMode,
|
|
|
|
// Privacy & Security
|
|
sessionTimeout: 0, // Never
|
|
trustedSenders: [] as string[],
|
|
|
|
// Calendar Notifications
|
|
calendarNotificationsEnabled: true,
|
|
calendarNotificationSound: true,
|
|
|
|
// Experimental
|
|
senderFavicons: true,
|
|
|
|
// Folders
|
|
folderIcons: {} as Record<string, string>,
|
|
|
|
// Advanced
|
|
debugMode: false,
|
|
};
|
|
|
|
export const useSettingsStore = create<SettingsState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
...DEFAULT_SETTINGS,
|
|
|
|
updateSetting: (key, value) => {
|
|
set({ [key]: value });
|
|
|
|
// Apply font size to document root
|
|
if (key === 'fontSize') {
|
|
applyFontSize(value as FontSize);
|
|
}
|
|
|
|
// Apply list density to document root
|
|
if (key === 'listDensity') {
|
|
applyListDensity(value as ListDensity);
|
|
}
|
|
|
|
// Apply animations to document root
|
|
if (key === 'animationsEnabled') {
|
|
applyAnimations(value as boolean);
|
|
}
|
|
},
|
|
|
|
resetToDefaults: () => {
|
|
set(DEFAULT_SETTINGS);
|
|
applyFontSize(DEFAULT_SETTINGS.fontSize);
|
|
applyListDensity(DEFAULT_SETTINGS.listDensity);
|
|
applyAnimations(DEFAULT_SETTINGS.animationsEnabled);
|
|
},
|
|
|
|
exportSettings: () => {
|
|
const state = get();
|
|
const settings = {
|
|
fontSize: state.fontSize,
|
|
listDensity: state.listDensity,
|
|
animationsEnabled: state.animationsEnabled,
|
|
dateFormat: state.dateFormat,
|
|
timeFormat: state.timeFormat,
|
|
firstDayOfWeek: state.firstDayOfWeek,
|
|
markAsReadDelay: state.markAsReadDelay,
|
|
deleteAction: state.deleteAction,
|
|
showPreview: state.showPreview,
|
|
emailsPerPage: state.emailsPerPage,
|
|
externalContentPolicy: state.externalContentPolicy,
|
|
trustedSenders: state.trustedSenders,
|
|
autoSaveDraftInterval: state.autoSaveDraftInterval,
|
|
sendConfirmation: state.sendConfirmation,
|
|
defaultReplyMode: state.defaultReplyMode,
|
|
sessionTimeout: state.sessionTimeout,
|
|
calendarNotificationsEnabled: state.calendarNotificationsEnabled,
|
|
calendarNotificationSound: state.calendarNotificationSound,
|
|
senderFavicons: state.senderFavicons,
|
|
folderIcons: state.folderIcons,
|
|
debugMode: state.debugMode,
|
|
};
|
|
return JSON.stringify(settings, null, 2);
|
|
},
|
|
|
|
importSettings: (json: string) => {
|
|
try {
|
|
const settings = JSON.parse(json);
|
|
|
|
// Validate settings
|
|
if (typeof settings !== 'object' || settings === null) {
|
|
return false;
|
|
}
|
|
|
|
// Apply settings
|
|
Object.keys(settings).forEach((key) => {
|
|
if (key in DEFAULT_SETTINGS) {
|
|
set({ [key]: settings[key] });
|
|
}
|
|
});
|
|
|
|
// Apply visual settings
|
|
applyFontSize(get().fontSize);
|
|
applyListDensity(get().listDensity);
|
|
applyAnimations(get().animationsEnabled);
|
|
|
|
return true;
|
|
} catch (error) {
|
|
console.error('Failed to import settings:', error);
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// 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();
|
|
const current = get().trustedSenders;
|
|
if (!current.includes(normalizedEmail)) {
|
|
set({ trustedSenders: [...current, normalizedEmail] });
|
|
}
|
|
},
|
|
|
|
removeTrustedSender: (email: string) => {
|
|
const normalizedEmail = email.toLowerCase().trim();
|
|
set({
|
|
trustedSenders: get().trustedSenders.filter(e => e !== normalizedEmail)
|
|
});
|
|
},
|
|
|
|
isSenderTrusted: (email: string) => {
|
|
const normalizedEmail = email.toLowerCase().trim();
|
|
return get().trustedSenders.includes(normalizedEmail);
|
|
},
|
|
}),
|
|
{
|
|
name: 'settings-storage',
|
|
version: 1,
|
|
}
|
|
)
|
|
);
|
|
|
|
// Helper functions to apply settings to DOM
|
|
function applyFontSize(size: FontSize) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const sizeMap = {
|
|
small: '14px',
|
|
medium: '16px',
|
|
large: '18px',
|
|
};
|
|
root.style.setProperty('--font-size-base', sizeMap[size]);
|
|
}
|
|
|
|
function applyListDensity(density: ListDensity) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
const densityMap = {
|
|
compact: '32px',
|
|
regular: '48px',
|
|
comfortable: '64px',
|
|
};
|
|
root.style.setProperty('--list-item-height', densityMap[density]);
|
|
}
|
|
|
|
function applyAnimations(enabled: boolean) {
|
|
if (typeof document === 'undefined') return;
|
|
|
|
const root = document.documentElement;
|
|
if (enabled) {
|
|
root.style.removeProperty('--transition-duration');
|
|
} else {
|
|
root.style.setProperty('--transition-duration', '0s');
|
|
}
|
|
}
|
|
|
|
// Initialize settings on load
|
|
if (typeof window !== 'undefined') {
|
|
const store = useSettingsStore.getState();
|
|
applyFontSize(store.fontSize);
|
|
applyListDensity(store.listDensity);
|
|
applyAnimations(store.animationsEnabled);
|
|
}
|