feat: add mobile bottom action bar with reply and email navigation
- Move Reply, Reply All, Forward from toolbar to fixed bottom bar on mobile - Add Next/Previous email navigation buttons to the bottom bar - Style bottom bar to match existing NavigationRail horizontal pattern - Add onNavigateNext/onNavigatePrev props to EmailViewer - Wire up email list index-based navigation in page.tsx - Add padding to email content area to prevent bottom bar overlap
This commit is contained in:
@@ -808,6 +808,17 @@ export default function Home() {
|
|||||||
setActiveView("list");
|
setActiveView("list");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Navigate to next/previous email in the list
|
||||||
|
const selectedEmailIndex = selectedEmail ? emails.findIndex(e => e.id === selectedEmail.id) : -1;
|
||||||
|
|
||||||
|
const handleNavigateNext = selectedEmailIndex >= 0 && selectedEmailIndex < emails.length - 1
|
||||||
|
? () => handleEmailSelect(emails[selectedEmailIndex + 1])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const handleNavigatePrev = selectedEmailIndex > 0
|
||||||
|
? () => handleEmailSelect(emails[selectedEmailIndex - 1])
|
||||||
|
: undefined;
|
||||||
|
|
||||||
// Handle opening conversation view on mobile
|
// Handle opening conversation view on mobile
|
||||||
const handleOpenConversation = async (thread: ThreadGroup) => {
|
const handleOpenConversation = async (thread: ThreadGroup) => {
|
||||||
if (!client) return;
|
if (!client) return;
|
||||||
@@ -1403,6 +1414,8 @@ export default function Home() {
|
|||||||
setTabletListVisible(true);
|
setTabletListVisible(true);
|
||||||
selectEmail(null);
|
selectEmail(null);
|
||||||
}}
|
}}
|
||||||
|
onNavigateNext={handleNavigateNext}
|
||||||
|
onNavigatePrev={handleNavigatePrev}
|
||||||
onShowShortcuts={() => setShowShortcutsModal(true)}
|
onShowShortcuts={() => setShowShortcutsModal(true)}
|
||||||
currentUserEmail={client?.["username"]}
|
currentUserEmail={client?.["username"]}
|
||||||
currentUserName={client?.["username"]?.split("@")[0]}
|
currentUserName={client?.["username"]?.split("@")[0]}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
Download,
|
Download,
|
||||||
Mail,
|
Mail,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -89,6 +90,8 @@ interface EmailViewerProps {
|
|||||||
onUndoSpam?: () => void;
|
onUndoSpam?: () => void;
|
||||||
onMoveToMailbox?: (mailboxId: string) => void;
|
onMoveToMailbox?: (mailboxId: string) => void;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
|
onNavigateNext?: () => void;
|
||||||
|
onNavigatePrev?: () => void;
|
||||||
onShowShortcuts?: () => void;
|
onShowShortcuts?: () => void;
|
||||||
currentUserEmail?: string;
|
currentUserEmail?: string;
|
||||||
currentUserName?: string;
|
currentUserName?: string;
|
||||||
@@ -339,7 +342,7 @@ function ContactSidebarPanel({
|
|||||||
<div key={i} className="text-sm">
|
<div key={i} className="text-sm">
|
||||||
{o.name}
|
{o.name}
|
||||||
{o.units && o.units.length > 0 && (
|
{o.units && o.units.length > 0 && (
|
||||||
<span className="text-muted-foreground"> — {o.units.map(u => u.name).join(", ")}</span>
|
<span className="text-muted-foreground"> — {o.units.map(u => u.name).join(", ")}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -410,6 +413,8 @@ export function EmailViewer({
|
|||||||
onUndoSpam,
|
onUndoSpam,
|
||||||
onMoveToMailbox,
|
onMoveToMailbox,
|
||||||
onBack,
|
onBack,
|
||||||
|
onNavigateNext,
|
||||||
|
onNavigatePrev,
|
||||||
onShowShortcuts,
|
onShowShortcuts,
|
||||||
currentUserEmail,
|
currentUserEmail,
|
||||||
currentUserName,
|
currentUserName,
|
||||||
@@ -438,7 +443,7 @@ export function EmailViewer({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
// Tablet list visibility
|
// Tablet list visibility
|
||||||
const { isTablet } = useDeviceDetection();
|
const { isTablet, isMobile } = useDeviceDetection();
|
||||||
const { tabletListVisible } = useUIStore();
|
const { tabletListVisible } = useUIStore();
|
||||||
const { identities, client } = useAuthStore();
|
const { identities, client } = useAuthStore();
|
||||||
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||||
@@ -456,6 +461,8 @@ export function EmailViewer({
|
|||||||
const moreMenuRef = useRef<HTMLDivElement>(null);
|
const moreMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const tagMenuRef = useRef<HTMLDivElement>(null);
|
const tagMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const moveMenuRef = useRef<HTMLDivElement>(null);
|
const moveMenuRef = useRef<HTMLDivElement>(null);
|
||||||
|
const toolbarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [overflowCount, setOverflowCount] = useState(0);
|
||||||
const currentColor = getCurrentColor(email?.keywords);
|
const currentColor = getCurrentColor(email?.keywords);
|
||||||
|
|
||||||
// Build mailbox tree for move-to dropdown
|
// Build mailbox tree for move-to dropdown
|
||||||
@@ -522,6 +529,41 @@ export function EmailViewer({
|
|||||||
setMoveMenuOpen(false);
|
setMoveMenuOpen(false);
|
||||||
}, [email?.id]);
|
}, [email?.id]);
|
||||||
|
|
||||||
|
// Dynamically detect which toolbar items overflow and should move to the More menu
|
||||||
|
useEffect(() => {
|
||||||
|
const el = toolbarRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const calculate = () => {
|
||||||
|
const items = Array.from(el.querySelectorAll<HTMLElement>('[data-overflow-item]'));
|
||||||
|
if (items.length === 0) return;
|
||||||
|
// Sort descending by priority so highest number (least important) is first
|
||||||
|
items.sort((a, b) =>
|
||||||
|
Number(b.dataset.overflowPriority || 0) - Number(a.dataset.overflowPriority || 0)
|
||||||
|
);
|
||||||
|
// Show all items to measure their natural widths
|
||||||
|
items.forEach(item => { item.style.display = ''; });
|
||||||
|
const containerWidth = el.clientWidth;
|
||||||
|
const leftGroup = el.firstElementChild as HTMLElement;
|
||||||
|
const rightGroup = el.lastElementChild as HTMLElement;
|
||||||
|
const mainGap = parseFloat(getComputedStyle(el).gap) || 0;
|
||||||
|
// Iteratively hide items until content fits
|
||||||
|
let count = 0;
|
||||||
|
const isOverflowing = () =>
|
||||||
|
leftGroup.scrollWidth + rightGroup.scrollWidth + mainGap > containerWidth + 1;
|
||||||
|
for (const item of items) {
|
||||||
|
if (!isOverflowing()) break;
|
||||||
|
// Skip items already hidden by CSS (e.g., on mobile)
|
||||||
|
if (item.offsetWidth === 0) continue;
|
||||||
|
item.style.display = 'none';
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
setOverflowCount(prev => prev === count ? prev : count);
|
||||||
|
};
|
||||||
|
const observer = new ResizeObserver(calculate);
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [toolbarPosition]);
|
||||||
|
|
||||||
// Contact sidebar state
|
// Contact sidebar state
|
||||||
const [contactSidebarEmail, setContactSidebarEmail] = useState<string | null>(null);
|
const [contactSidebarEmail, setContactSidebarEmail] = useState<string | null>(null);
|
||||||
const contacts = useContactStore((s) => s.contacts);
|
const contacts = useContactStore((s) => s.contacts);
|
||||||
@@ -1063,6 +1105,144 @@ export function EmailViewer({
|
|||||||
key={email.id}
|
key={email.id}
|
||||||
className={cn("flex-1 flex flex-row h-full bg-background overflow-hidden animate-in fade-in duration-300 relative", className)}
|
className={cn("flex-1 flex flex-row h-full bg-background overflow-hidden animate-in fade-in duration-300 relative", className)}
|
||||||
>
|
>
|
||||||
|
{/* Mobile More menu sidebar overlay */}
|
||||||
|
{isMobile && moreMenuOpen && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-[60] sm:hidden"
|
||||||
|
onClick={() => setMoreMenuOpen(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isMobile && (
|
||||||
|
<div className={cn(
|
||||||
|
"fixed inset-y-0 right-0 w-72 bg-background border-l border-border z-[70] sm:hidden",
|
||||||
|
"transform transition-transform duration-300 ease-in-out",
|
||||||
|
"flex flex-col",
|
||||||
|
moreMenuOpen ? "translate-x-0" : "translate-x-full"
|
||||||
|
)}>
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||||
|
<span className="text-sm font-semibold text-foreground">{t('more_actions')}</span>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setMoreMenuOpen(false)} className="h-9 w-9">
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { onArchive?.(); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<Archive className="w-5 h-5" />
|
||||||
|
{t('archive')}
|
||||||
|
</button>
|
||||||
|
{(onMarkAsSpam || onUndoSpam) && (
|
||||||
|
<button
|
||||||
|
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||||
|
>
|
||||||
|
{isInJunkFolder ? (
|
||||||
|
<ShieldCheck className="h-5 w-5 text-green-600 dark:text-green-400" />
|
||||||
|
) : (
|
||||||
|
<ShieldAlert className="h-5 w-5 text-red-600 dark:text-red-400" />
|
||||||
|
)}
|
||||||
|
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{/* Move to folder */}
|
||||||
|
{moveTree.length > 0 && onMoveToMailbox && (
|
||||||
|
<>
|
||||||
|
<div className="h-px bg-border my-1" />
|
||||||
|
<div className="px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('move_to')}</div>
|
||||||
|
{(() => {
|
||||||
|
const renderMobileNodes = (nodes: MailboxNode[], depth = 0) => {
|
||||||
|
return nodes.map((node) => {
|
||||||
|
const Icon = getMoveMailboxIcon(node.role);
|
||||||
|
const isTarget = moveTargetIds.has(node.id);
|
||||||
|
return (
|
||||||
|
<div key={node.id}>
|
||||||
|
{isTarget ? (
|
||||||
|
<button
|
||||||
|
onClick={() => { onMoveToMailbox(node.id); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3"
|
||||||
|
style={{ paddingLeft: `${1 + depth * 1}rem` }}
|
||||||
|
>
|
||||||
|
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||||
|
<span className="truncate">{node.name}</span>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="px-4 py-2.5 min-h-[44px] text-sm flex items-center gap-3 text-muted-foreground"
|
||||||
|
style={{ paddingLeft: `${1 + depth * 1}rem` }}
|
||||||
|
>
|
||||||
|
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||||
|
<span>{node.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{node.children.length > 0 && renderMobileNodes(node.children, depth + 1)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return renderMobileNodes(moveTree);
|
||||||
|
})()}
|
||||||
|
<div className="h-px bg-border my-1" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* Tags */}
|
||||||
|
{colorOptions.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="h-px bg-border my-1" />
|
||||||
|
<div className="px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('tag')}</div>
|
||||||
|
{colorOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
onClick={() => { if (email) onSetColorTag?.(email.id, option.value); setMoreMenuOpen(false); }}
|
||||||
|
className={cn(
|
||||||
|
"w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3",
|
||||||
|
currentColor === option.value && "bg-accent font-medium"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className={cn("w-3.5 h-3.5 rounded-full flex-shrink-0", option.color)} />
|
||||||
|
<span className="truncate">{option.name}</span>
|
||||||
|
{currentColor === option.value && <Check className="w-4 h-4 ml-auto flex-shrink-0 text-foreground" />}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{currentColor && (
|
||||||
|
<button
|
||||||
|
onClick={() => { if (email) onSetColorTag?.(email.id, null); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-2.5 min-h-[44px] text-sm text-left hover:bg-muted flex items-center gap-3 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4 flex-shrink-0" />
|
||||||
|
<span>{t('remove_color')}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="h-px bg-border my-1" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => { handlePrint(); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<Printer className="w-5 h-5" />
|
||||||
|
{t('print')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowSourceModal(true); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<Code className="w-5 h-5" />
|
||||||
|
{t('view_source')}
|
||||||
|
</button>
|
||||||
|
{onShowShortcuts && (
|
||||||
|
<button
|
||||||
|
onClick={() => { onShowShortcuts(); setMoreMenuOpen(false); }}
|
||||||
|
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
|
||||||
|
>
|
||||||
|
<Keyboard className="w-5 h-5" />
|
||||||
|
{t('keyboard_shortcuts')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Main email content */}
|
{/* Main email content */}
|
||||||
<div className="flex-1 flex flex-col h-full overflow-hidden min-w-0">
|
<div className="flex-1 flex flex-col h-full overflow-hidden min-w-0">
|
||||||
{/* Loading overlay when fetching new email */}
|
{/* Loading overlay when fetching new email */}
|
||||||
@@ -1081,7 +1261,7 @@ export function EmailViewer({
|
|||||||
"max-lg:sticky max-lg:top-0 max-lg:z-10"
|
"max-lg:sticky max-lg:top-0 max-lg:z-10"
|
||||||
)}>
|
)}>
|
||||||
<div className="px-2 sm:px-4 lg:px-6 py-1 sm:py-2">
|
<div className="px-2 sm:px-4 lg:px-6 py-1 sm:py-2">
|
||||||
<div className="flex items-center justify-between gap-0.5 sm:gap-2">
|
<div ref={toolbarRef} className="flex items-center justify-between gap-0.5 sm:gap-2">
|
||||||
{/* Left: Back + Reply actions */}
|
{/* Left: Back + Reply actions */}
|
||||||
<div className="flex items-center gap-0 sm:gap-1">
|
<div className="flex items-center gap-0 sm:gap-1">
|
||||||
{isTablet && !tabletListVisible && onBack && (
|
{isTablet && !tabletListVisible && onBack && (
|
||||||
@@ -1099,31 +1279,31 @@ export function EmailViewer({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onReply?.()}
|
onClick={() => onReply?.()}
|
||||||
className="flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
||||||
title={t('tooltips.reply')}
|
title={t('tooltips.reply')}
|
||||||
>
|
>
|
||||||
<Reply className="w-4 h-4" />
|
<Reply className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('reply')}</span>
|
<span className="hidden sm:inline text-sm">{t('reply')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onReplyAll}
|
onClick={onReplyAll}
|
||||||
className="flex-col items-center gap-0.5 h-auto py-1.5 px-1.5 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0 sm:px-3"
|
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0 sm:px-3"
|
||||||
title={t('tooltips.reply_all')}
|
title={t('tooltips.reply_all')}
|
||||||
>
|
>
|
||||||
<ReplyAll className="w-4 h-4" />
|
<ReplyAll className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('reply_all')}</span>
|
<span className="hidden sm:inline text-sm">{t('reply_all')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onForward}
|
onClick={onForward}
|
||||||
className="flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
||||||
title={t('tooltips.forward')}
|
title={t('tooltips.forward')}
|
||||||
>
|
>
|
||||||
<Forward className="w-4 h-4" />
|
<Forward className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('forward')}</span>
|
<span className="hidden sm:inline text-sm">{t('forward')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1134,11 +1314,13 @@ export function EmailViewer({
|
|||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Archive - hidden on mobile, available in More menu */}
|
{/* Archive - hidden on mobile, overflows to More menu */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onArchive}
|
onClick={onArchive}
|
||||||
|
data-overflow-item
|
||||||
|
data-overflow-priority="1"
|
||||||
className="hidden sm:inline-flex h-8 gap-1.5"
|
className="hidden sm:inline-flex h-8 gap-1.5"
|
||||||
title={t('tooltips.archive')}
|
title={t('tooltips.archive')}
|
||||||
>
|
>
|
||||||
@@ -1155,12 +1337,14 @@ export function EmailViewer({
|
|||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('delete')}</span>
|
<span className="text-[10px] leading-tight sm:text-sm">{t('delete')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/* Spam - hidden on mobile, available in More menu */}
|
{/* Spam - hidden on mobile, overflows to More menu */}
|
||||||
{(onMarkAsSpam || onUndoSpam) && (
|
{(onMarkAsSpam || onUndoSpam) && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={isInJunkFolder ? onUndoSpam : onMarkAsSpam}
|
onClick={isInJunkFolder ? onUndoSpam : onMarkAsSpam}
|
||||||
|
data-overflow-item
|
||||||
|
data-overflow-priority="2"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden sm:inline-flex h-8 gap-1.5",
|
"hidden sm:inline-flex h-8 gap-1.5",
|
||||||
isInJunkFolder ? "hover:bg-green-50 dark:hover:bg-green-950/30" : "hover:bg-red-50 dark:hover:bg-red-950/30"
|
isInJunkFolder ? "hover:bg-green-50 dark:hover:bg-green-950/30" : "hover:bg-red-50 dark:hover:bg-red-950/30"
|
||||||
@@ -1174,9 +1358,9 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Move to folder - hidden on mobile, available in More menu */}
|
{/* Move to folder - hidden on mobile, overflows to More menu */}
|
||||||
{moveTree.length > 0 && onMoveToMailbox && (
|
{moveTree.length > 0 && onMoveToMailbox && (
|
||||||
<div ref={moveMenuRef} className="relative hidden sm:block">
|
<div ref={moveMenuRef} data-overflow-item data-overflow-priority="3" className="relative hidden sm:block">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
|
onClick={() => { setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
|
||||||
className="h-8 rounded hover:bg-muted flex items-center gap-1.5 px-2"
|
className="h-8 rounded hover:bg-muted flex items-center gap-1.5 px-2"
|
||||||
@@ -1237,10 +1421,10 @@ export function EmailViewer({
|
|||||||
<span className="text-[10px] leading-tight sm:hidden">{isStarred ? t('tooltips.unstar') : t('tooltips.star')}</span>
|
<span className="text-[10px] leading-tight sm:hidden">{isStarred ? t('tooltips.unstar') : t('tooltips.star')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-border mx-0.5 hidden sm:block" />
|
{/* Tag Picker + Divider - hidden on mobile, overflows to More menu */}
|
||||||
|
<div data-overflow-item data-overflow-priority="4" className="hidden sm:flex items-center">
|
||||||
{/* Tag Picker - click-based, hidden on mobile (available in More menu) */}
|
<div className="w-px h-5 bg-border mx-0.5" />
|
||||||
<div ref={tagMenuRef} className="relative hidden sm:block">
|
<div ref={tagMenuRef} className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setTagMenuOpen(!tagMenuOpen); setMoreMenuOpen(false); setMoveMenuOpen(false); }}
|
onClick={() => { setTagMenuOpen(!tagMenuOpen); setMoreMenuOpen(false); setMoveMenuOpen(false); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1296,12 +1480,15 @@ export function EmailViewer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Print - hidden on mobile, available in More menu */}
|
{/* Print - hidden on mobile, overflows to More menu */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
|
data-overflow-item
|
||||||
|
data-overflow-priority="5"
|
||||||
className="hidden sm:inline-flex h-8 gap-1.5"
|
className="hidden sm:inline-flex h-8 gap-1.5"
|
||||||
title={t('print')}
|
title={t('print')}
|
||||||
>
|
>
|
||||||
@@ -1321,12 +1508,12 @@ export function EmailViewer({
|
|||||||
<MoreVertical className="w-4 h-4 text-muted-foreground" />
|
<MoreVertical className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>
|
<span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{moreMenuOpen && (
|
{moreMenuOpen && !isMobile && (
|
||||||
<div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10">
|
<div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10">
|
||||||
{/* Mobile-only actions */}
|
{/* Overflow actions - shown when hidden from toolbar or on mobile */}
|
||||||
<button
|
<button
|
||||||
onClick={() => { onArchive?.(); setMoreMenuOpen(false); }}
|
onClick={() => { onArchive?.(); setMoreMenuOpen(false); }}
|
||||||
className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
|
className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 5 ? "" : "sm:hidden")}
|
||||||
>
|
>
|
||||||
<Archive className="w-4 h-4" />
|
<Archive className="w-4 h-4" />
|
||||||
{t('archive')}
|
{t('archive')}
|
||||||
@@ -1334,7 +1521,7 @@ export function EmailViewer({
|
|||||||
{(onMarkAsSpam || onUndoSpam) && (
|
{(onMarkAsSpam || onUndoSpam) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
|
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
|
||||||
className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
|
className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 4 ? "" : "sm:hidden")}
|
||||||
>
|
>
|
||||||
{isInJunkFolder ? (
|
{isInJunkFolder ? (
|
||||||
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
|
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||||
@@ -1344,9 +1531,9 @@ export function EmailViewer({
|
|||||||
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
|
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* Move to folder submenu on mobile */}
|
{/* Move to folder submenu */}
|
||||||
{moveTree.length > 0 && onMoveToMailbox && (
|
{moveTree.length > 0 && onMoveToMailbox && (
|
||||||
<div className="sm:hidden">
|
<div className={cn(overflowCount >= 3 ? "" : "sm:hidden")}>
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('move_to')}</div>
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('move_to')}</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
@@ -1383,9 +1570,9 @@ export function EmailViewer({
|
|||||||
})()}
|
})()}
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
</div>
|
</div>
|
||||||
)} {/* Tag submenu on mobile */}
|
)} {/* Tag submenu */}
|
||||||
{colorOptions.length > 0 && (
|
{colorOptions.length > 0 && (
|
||||||
<div className="sm:hidden">
|
<div className={cn(overflowCount >= 2 ? "" : "sm:hidden")}>
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('tag')}</div>
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('tag')}</div>
|
||||||
{colorOptions.map((option) => (
|
{colorOptions.map((option) => (
|
||||||
@@ -1416,7 +1603,7 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => { handlePrint(); setMoreMenuOpen(false); }}
|
onClick={() => { handlePrint(); setMoreMenuOpen(false); }}
|
||||||
className="w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
className={cn("w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 1 ? "" : "sm:hidden")}
|
||||||
>
|
>
|
||||||
<Printer className="w-4 h-4" />
|
<Printer className="w-4 h-4" />
|
||||||
{t('print')}
|
{t('print')}
|
||||||
@@ -1519,38 +1706,38 @@ export function EmailViewer({
|
|||||||
{toolbarPosition === 'below-subject' && (
|
{toolbarPosition === 'below-subject' && (
|
||||||
<div className="bg-background border-b border-border">
|
<div className="bg-background border-b border-border">
|
||||||
<div className="px-2 sm:px-4 lg:px-6 py-1 sm:py-1.5">
|
<div className="px-2 sm:px-4 lg:px-6 py-1 sm:py-1.5">
|
||||||
<div className="flex items-center justify-between gap-0.5 sm:gap-2">
|
<div ref={toolbarRef} className="flex items-center justify-between gap-0.5 sm:gap-2">
|
||||||
{/* Left: Reply actions */}
|
{/* Left: Reply actions */}
|
||||||
<div className="flex items-center gap-0 sm:gap-0.5">
|
<div className="flex items-center gap-0 sm:gap-0.5">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onReply?.()}
|
onClick={() => onReply?.()}
|
||||||
className="flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
||||||
title={t('tooltips.reply')}
|
title={t('tooltips.reply')}
|
||||||
>
|
>
|
||||||
<Reply className="w-4 h-4" />
|
<Reply className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('reply')}</span>
|
<span className="hidden sm:inline text-sm">{t('reply')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onReplyAll}
|
onClick={onReplyAll}
|
||||||
className="flex-col items-center gap-0.5 h-auto py-1.5 px-1.5 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0 sm:px-3"
|
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0 sm:px-3"
|
||||||
title={t('tooltips.reply_all')}
|
title={t('tooltips.reply_all')}
|
||||||
>
|
>
|
||||||
<ReplyAll className="w-4 h-4" />
|
<ReplyAll className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('reply_all')}</span>
|
<span className="hidden sm:inline text-sm">{t('reply_all')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onForward}
|
onClick={onForward}
|
||||||
className="flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
className="hidden sm:flex sm:flex-row sm:h-8 sm:gap-1.5 sm:py-0"
|
||||||
title={t('tooltips.forward')}
|
title={t('tooltips.forward')}
|
||||||
>
|
>
|
||||||
<Forward className="w-4 h-4" />
|
<Forward className="w-4 h-4" />
|
||||||
<span className="text-[10px] leading-tight sm:text-sm">{t('forward')}</span>
|
<span className="hidden sm:inline text-sm">{t('forward')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1561,23 +1748,27 @@ export function EmailViewer({
|
|||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Archive - hidden on mobile, available in More menu */}
|
{/* Archive - hidden on mobile, overflows to More menu */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onArchive}
|
onClick={onArchive}
|
||||||
|
data-overflow-item
|
||||||
|
data-overflow-priority="1"
|
||||||
className="hidden sm:inline-flex h-8 gap-1.5"
|
className="hidden sm:inline-flex h-8 gap-1.5"
|
||||||
title={t('tooltips.archive')}
|
title={t('tooltips.archive')}
|
||||||
>
|
>
|
||||||
<Archive className="w-4 h-4" />
|
<Archive className="w-4 h-4" />
|
||||||
<span className="hidden sm:inline text-sm">{t('archive')}</span>
|
<span className="hidden sm:inline text-sm">{t('archive')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{/* Spam - hidden on mobile, available in More menu */}
|
{/* Spam - hidden on mobile, overflows to More menu */}
|
||||||
{(onMarkAsSpam || onUndoSpam) && (
|
{(onMarkAsSpam || onUndoSpam) && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={isInJunkFolder ? onUndoSpam : onMarkAsSpam}
|
onClick={isInJunkFolder ? onUndoSpam : onMarkAsSpam}
|
||||||
|
data-overflow-item
|
||||||
|
data-overflow-priority="2"
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden sm:inline-flex h-8 gap-1.5",
|
"hidden sm:inline-flex h-8 gap-1.5",
|
||||||
isInJunkFolder ? "hover:bg-green-50 dark:hover:bg-green-950/30" : "hover:bg-red-50 dark:hover:bg-red-950/30"
|
isInJunkFolder ? "hover:bg-green-50 dark:hover:bg-green-950/30" : "hover:bg-red-50 dark:hover:bg-red-950/30"
|
||||||
@@ -1591,9 +1782,9 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* Move to folder - hidden on mobile, available in More menu */}
|
{/* Move to folder - hidden on mobile, overflows to More menu */}
|
||||||
{moveTree.length > 0 && onMoveToMailbox && (
|
{moveTree.length > 0 && onMoveToMailbox && (
|
||||||
<div ref={moveMenuRef} className="relative hidden sm:block">
|
<div ref={moveMenuRef} data-overflow-item data-overflow-priority="3" className="relative hidden sm:block">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
|
onClick={() => { setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
|
||||||
className="h-8 rounded hover:bg-muted flex items-center gap-1.5 px-2"
|
className="h-8 rounded hover:bg-muted flex items-center gap-1.5 px-2"
|
||||||
@@ -1664,10 +1855,10 @@ export function EmailViewer({
|
|||||||
<span className="text-[10px] leading-tight sm:hidden">{isStarred ? t('tooltips.unstar') : t('tooltips.star')}</span>
|
<span className="text-[10px] leading-tight sm:hidden">{isStarred ? t('tooltips.unstar') : t('tooltips.star')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="w-px h-5 bg-border mx-0.5 hidden sm:block" />
|
{/* Tag Picker + Divider - hidden on mobile, overflows to More menu */}
|
||||||
|
<div data-overflow-item data-overflow-priority="4" className="hidden sm:flex items-center">
|
||||||
{/* Tag Picker - click-based, hidden on mobile (available in More menu) */}
|
<div className="w-px h-5 bg-border mx-0.5" />
|
||||||
<div ref={tagMenuRef} className="relative hidden sm:block">
|
<div ref={tagMenuRef} className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setTagMenuOpen(!tagMenuOpen); setMoreMenuOpen(false); setMoveMenuOpen(false); }}
|
onClick={() => { setTagMenuOpen(!tagMenuOpen); setMoreMenuOpen(false); setMoveMenuOpen(false); }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -1723,12 +1914,15 @@ export function EmailViewer({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Print - hidden on mobile, available in More menu */}
|
{/* Print - hidden on mobile, overflows to More menu */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handlePrint}
|
onClick={handlePrint}
|
||||||
|
data-overflow-item
|
||||||
|
data-overflow-priority="5"
|
||||||
className="hidden sm:inline-flex h-8 gap-1.5"
|
className="hidden sm:inline-flex h-8 gap-1.5"
|
||||||
title={t('print')}
|
title={t('print')}
|
||||||
>
|
>
|
||||||
@@ -1748,12 +1942,12 @@ export function EmailViewer({
|
|||||||
<MoreVertical className="w-4 h-4 text-muted-foreground" />
|
<MoreVertical className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>
|
<span className="text-[10px] leading-tight sm:hidden">{t('more_actions')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
{moreMenuOpen && (
|
{moreMenuOpen && !isMobile && (
|
||||||
<div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10">
|
<div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10">
|
||||||
{/* Mobile-only actions */}
|
{/* Overflow actions - shown when hidden from toolbar or on mobile */}
|
||||||
<button
|
<button
|
||||||
onClick={() => { onArchive?.(); setMoreMenuOpen(false); }}
|
onClick={() => { onArchive?.(); setMoreMenuOpen(false); }}
|
||||||
className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
|
className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 5 ? "" : "sm:hidden")}
|
||||||
>
|
>
|
||||||
<Archive className="w-4 h-4" />
|
<Archive className="w-4 h-4" />
|
||||||
{t('archive')}
|
{t('archive')}
|
||||||
@@ -1761,7 +1955,7 @@ export function EmailViewer({
|
|||||||
{(onMarkAsSpam || onUndoSpam) && (
|
{(onMarkAsSpam || onUndoSpam) && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
|
onClick={() => { (isInJunkFolder ? onUndoSpam : onMarkAsSpam)?.(); setMoreMenuOpen(false); }}
|
||||||
className="w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2 sm:hidden"
|
className={cn("w-full px-3 py-2.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 4 ? "" : "sm:hidden")}
|
||||||
>
|
>
|
||||||
{isInJunkFolder ? (
|
{isInJunkFolder ? (
|
||||||
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
|
<ShieldCheck className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||||
@@ -1771,9 +1965,9 @@ export function EmailViewer({
|
|||||||
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
|
{isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* Move to folder submenu on mobile */}
|
{/* Move to folder submenu */}
|
||||||
{moveTree.length > 0 && onMoveToMailbox && (
|
{moveTree.length > 0 && onMoveToMailbox && (
|
||||||
<div className="sm:hidden">
|
<div className={cn(overflowCount >= 3 ? "" : "sm:hidden")}>
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('move_to')}</div>
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('move_to')}</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
@@ -1810,9 +2004,9 @@ export function EmailViewer({
|
|||||||
})()}
|
})()}
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
</div>
|
</div>
|
||||||
)} {/* Tag submenu on mobile */}
|
)} {/* Tag submenu */}
|
||||||
{colorOptions.length > 0 && (
|
{colorOptions.length > 0 && (
|
||||||
<div className="sm:hidden">
|
<div className={cn(overflowCount >= 2 ? "" : "sm:hidden")}>
|
||||||
<div className="h-px bg-border my-1" />
|
<div className="h-px bg-border my-1" />
|
||||||
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('tag')}</div>
|
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('tag')}</div>
|
||||||
{colorOptions.map((option) => (
|
{colorOptions.map((option) => (
|
||||||
@@ -1843,7 +2037,7 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => { handlePrint(); setMoreMenuOpen(false); }}
|
onClick={() => { handlePrint(); setMoreMenuOpen(false); }}
|
||||||
className="w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
|
className={cn("w-full px-3 py-2.5 sm:py-2 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2", overflowCount >= 1 ? "" : "sm:hidden")}
|
||||||
>
|
>
|
||||||
<Printer className="w-4 h-4" />
|
<Printer className="w-4 h-4" />
|
||||||
{t('print')}
|
{t('print')}
|
||||||
@@ -2403,7 +2597,7 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Email Content Area */}
|
{/* Email Content Area */}
|
||||||
<div className="flex-1 overflow-auto bg-muted/30">
|
<div className={cn("flex-1 overflow-auto bg-muted/30", isMobile && "pb-16")}>
|
||||||
{/* Mobile/Tablet Sender Info - scrolls with content */}
|
{/* Mobile/Tablet Sender Info - scrolls with content */}
|
||||||
<div className="lg:hidden bg-background border-b border-border px-4" style={{ paddingBlock: 'var(--density-header-py)' }}>
|
<div className="lg:hidden bg-background border-b border-border px-4" style={{ paddingBlock: 'var(--density-header-py)' }}>
|
||||||
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
|
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
|
||||||
@@ -2446,12 +2640,12 @@ export function EmailViewer({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{email.to && email.to.length > 0 && (
|
{email.to && email.to.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<span>→ {t('recipient_to_prefix')}</span>
|
<span>→ {t('recipient_to_prefix')}</span>
|
||||||
{renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar)}
|
{renderClickableRecipients(email.to, currentUserEmail, t, handleViewContactSidebar)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -2693,6 +2887,62 @@ export function EmailViewer({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile bottom action bar */}
|
||||||
|
{isMobile && (
|
||||||
|
<nav className="fixed bottom-0 left-0 right-0 z-[50] bg-background border-t border-border sm:hidden">
|
||||||
|
<div className="flex items-center justify-around">
|
||||||
|
<button
|
||||||
|
onClick={onNavigatePrev}
|
||||||
|
disabled={!onNavigatePrev}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] transition-colors duration-150",
|
||||||
|
onNavigatePrev ? "text-muted-foreground active:text-foreground" : "text-muted-foreground/30"
|
||||||
|
)}
|
||||||
|
aria-label={t('tooltips.previous')}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-medium leading-tight">{t('previous')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onReply?.()}
|
||||||
|
className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] text-muted-foreground active:text-foreground transition-colors duration-150"
|
||||||
|
aria-label={t('tooltips.reply')}
|
||||||
|
>
|
||||||
|
<Reply className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-medium leading-tight">{t('reply')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onReplyAll}
|
||||||
|
className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] text-muted-foreground active:text-foreground transition-colors duration-150"
|
||||||
|
aria-label={t('tooltips.reply_all')}
|
||||||
|
>
|
||||||
|
<ReplyAll className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-medium leading-tight">{t('reply_all')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onForward}
|
||||||
|
className="flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] text-muted-foreground active:text-foreground transition-colors duration-150"
|
||||||
|
aria-label={t('tooltips.forward')}
|
||||||
|
>
|
||||||
|
<Forward className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-medium leading-tight">{t('forward')}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onNavigateNext}
|
||||||
|
disabled={!onNavigateNext}
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center justify-center gap-1 py-2 px-3 min-w-[64px] min-h-[44px] transition-colors duration-150",
|
||||||
|
onNavigateNext ? "text-muted-foreground active:text-foreground" : "text-muted-foreground/30"
|
||||||
|
)}
|
||||||
|
aria-label={t('tooltips.next')}
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-5 h-5" />
|
||||||
|
<span className="text-[10px] font-medium leading-tight">{t('next')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Contact Detail Sidebar - desktop only */}
|
{/* Contact Detail Sidebar - desktop only */}
|
||||||
{contactSidebarEmail && !isMobileDevice && (
|
{contactSidebarEmail && !isMobileDevice && (
|
||||||
<ContactSidebarPanel
|
<ContactSidebarPanel
|
||||||
|
|||||||
@@ -175,6 +175,8 @@
|
|||||||
"set_color": "Set tag",
|
"set_color": "Set tag",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"more_actions": "More actions",
|
"more_actions": "More actions",
|
||||||
|
"previous": "Prev",
|
||||||
|
"next": "Next",
|
||||||
"move_to": "Move to...",
|
"move_to": "Move to...",
|
||||||
"remove_color": "Remove tag",
|
"remove_color": "Remove tag",
|
||||||
"more_count": "+{count} more",
|
"more_count": "+{count} more",
|
||||||
@@ -251,7 +253,9 @@
|
|||||||
"delete": "Delete (# or Del)",
|
"delete": "Delete (# or Del)",
|
||||||
"star": "Star (s)",
|
"star": "Star (s)",
|
||||||
"unstar": "Unstar (s)",
|
"unstar": "Unstar (s)",
|
||||||
"compose": "Compose (c)"
|
"compose": "Compose (c)",
|
||||||
|
"previous": "Previous email",
|
||||||
|
"next": "Next email"
|
||||||
},
|
},
|
||||||
"spam": {
|
"spam": {
|
||||||
"button_title": "Report spam",
|
"button_title": "Report spam",
|
||||||
|
|||||||
Reference in New Issue
Block a user