refactor: rework mobile mail viewer toolbar

This commit is contained in:
Linus Rath
2026-05-01 21:00:34 +02:00
parent 231a9017d2
commit d7c29b7bec
17 changed files with 104 additions and 61 deletions
+24 -61
View File
@@ -3095,7 +3095,7 @@ export function EmailViewer({
<PluginSlot name="toolbar-actions" /> <PluginSlot name="toolbar-actions" />
</div> </div>
{/* Right: Organize actions - order: archive, delete, move, star, tag, spam, read state, print, view source */} {/* Right: Organize actions - order: archive, delete, move, tag, spam, read state, print, view source */}
<div className="flex items-center gap-0 sm:gap-0.5"> <div className="flex items-center gap-0 sm:gap-0.5">
{/* Archive */} {/* Archive */}
<Button <Button
@@ -3104,11 +3104,11 @@ export function EmailViewer({
onClick={onArchive} onClick={onArchive}
data-overflow-item data-overflow-item
data-overflow-priority="4" data-overflow-priority="4"
className="hidden sm:inline-flex h-8 gap-1.5" 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"
title={t('tooltips.archive')} title={t('tooltips.archive')}
> >
<Archive className="w-4 h-4" /> <Archive className="w-4 h-4" />
{showToolbarLabels && <span className="hidden sm:inline text-sm">{t('archive')}</span>} {showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('archive')}</span>}
</Button> </Button>
{/* Delete */} {/* Delete */}
<Button <Button
@@ -3123,16 +3123,16 @@ export function EmailViewer({
</Button> </Button>
{/* Move to folder */} {/* Move to folder */}
{moveTree.length > 0 && onMoveToMailbox && ( {moveTree.length > 0 && onMoveToMailbox && (
<div ref={moveMenuRef} data-overflow-item data-overflow-priority="5" className="relative hidden sm:block"> <div ref={moveMenuRef} data-overflow-item data-overflow-priority="5" className="relative">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => { setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }} onClick={() => { setMoveMenuOpen(!moveMenuOpen); setMoreMenuOpen(false); setTagMenuOpen(false); }}
className="h-8 gap-1.5" 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"
title={t('move_to')} title={t('move_to')}
> >
<FolderInput className="w-4 h-4" /> <FolderInput className="w-4 h-4" />
{showToolbarLabels && <span className="text-sm">{t('move_to')}</span>} {showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{t('move')}</span>}
</Button> </Button>
{moveMenuOpen && ( {moveMenuOpen && (
<div className="absolute right-0 top-full mt-1 py-1 w-48 max-h-72 overflow-y-auto bg-background rounded-lg shadow-lg border border-border z-10"> <div className="absolute right-0 top-full mt-1 py-1 w-48 max-h-72 overflow-y-auto bg-background rounded-lg shadow-lg border border-border z-10">
@@ -3172,21 +3172,6 @@ export function EmailViewer({
)} )}
</div> </div>
)} )}
{/* Star/Flag toggle */}
<Button
variant="ghost"
size="sm"
onClick={onToggleStar}
className="flex-col items-center gap-0.5 h-auto py-1.5 px-2 sm:flex-row sm:h-8 sm:w-auto sm:gap-1.5 sm:py-0 sm:px-2"
title={isStarred ? t('tooltips.unstar') : t('tooltips.star')}
>
<Star className={cn(
"w-4 h-4 transition-colors",
isStarred ? "fill-yellow-400 text-yellow-400" : "text-muted-foreground"
)} />
<span className="text-[10px] leading-tight sm:hidden">{isStarred ? t('tooltips.unstar') : t('tooltips.star')}</span>
</Button>
{/* Tag Picker - hidden on mobile, overflows to More menu */} {/* Tag Picker - hidden on mobile, overflows to More menu */}
<div data-overflow-item data-overflow-priority="6" className="hidden sm:flex items-center"> <div data-overflow-item data-overflow-priority="6" className="hidden sm:flex items-center">
<div className="w-px h-5 bg-border mx-0.5" /> <div className="w-px h-5 bg-border mx-0.5" />
@@ -3256,7 +3241,7 @@ export function EmailViewer({
</div> </div>
</div> </div>
{/* Spam - hidden on mobile, overflows to More menu */} {/* Spam */}
{(onMarkAsSpam || onUndoSpam) && ( {(onMarkAsSpam || onUndoSpam) && (
<Button <Button
variant="ghost" variant="ghost"
@@ -3265,7 +3250,7 @@ export function EmailViewer({
data-overflow-item data-overflow-item
data-overflow-priority="7" data-overflow-priority="7"
className={cn( className={cn(
"hidden sm:inline-flex h-8 gap-1.5", "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",
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"
)} )}
title={isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')} title={isInJunkFolder ? t('spam.not_spam_title') : t('spam.button_title')}
@@ -3275,20 +3260,22 @@ export function EmailViewer({
) : ( ) : (
<ShieldAlert className="h-4 w-4 text-red-600 dark:text-red-400" /> <ShieldAlert className="h-4 w-4 text-red-600 dark:text-red-400" />
)} )}
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{isInJunkFolder ? t('not_spam_short') : t('spam_short')}</span>}
</Button> </Button>
)} )}
{/* Toggle read state - hidden on mobile, overflows to More menu */} {/* Toggle read state */}
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => onMarkAsRead?.(email.id, isUnread)} onClick={() => onMarkAsRead?.(email.id, isUnread)}
data-overflow-item data-overflow-item
data-overflow-priority="8" data-overflow-priority="8"
className="hidden sm:inline-flex h-8 gap-1.5" 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"
title={isUnread ? t('mark_read') : t('mark_unread')} title={isUnread ? t('mark_read') : t('mark_unread')}
> >
{isUnread ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />} {isUnread ? <MailOpen className="w-4 h-4" /> : <Mail className="w-4 h-4" />}
{showToolbarLabels && <span className="text-[10px] leading-tight sm:text-sm">{isUnread ? t('read') : t('unread')}</span>}
</Button> </Button>
{/* Print - hidden on mobile, overflows to More menu */} {/* Print - hidden on mobile, overflows to More menu */}
@@ -3347,6 +3334,14 @@ export function EmailViewer({
</Button> </Button>
{moreMenuOpen && !isMobile && ( {moreMenuOpen && !isMobile && (
<div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10 py-1"> <div className="absolute right-0 top-full mt-1 w-48 bg-background rounded-md shadow-lg border border-border z-10 py-1">
{/* Star toggle */}
<button
onClick={() => { onToggleStar?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
className="w-full px-3 py-1.5 text-sm text-left hover:bg-muted text-foreground flex items-center gap-2"
>
<Star className={cn("w-4 h-4", isStarred && "fill-yellow-400 text-yellow-400")} />
{isStarred ? t('tooltips.unstar') : t('tooltips.star')}
</button>
{/* Overflow: reply */} {/* Overflow: reply */}
<button <button
onClick={() => { onReply?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }} onClick={() => { onReply?.(); setMoreMenuOpen(false); setMoreMenuSub(null); }}
@@ -3600,24 +3595,14 @@ export function EmailViewer({
<div className="flex-1 overflow-y-auto py-2"> <div className="flex-1 overflow-y-auto py-2">
{moreMenuSub === null && ( {moreMenuSub === null && (
<> <>
{/* Star toggle */}
<button <button
onClick={() => { onArchive?.(); setMoreMenuOpen(false); }} onClick={() => { onToggleStar?.(); 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" 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" /> <Star className={cn("w-5 h-5", isStarred && "fill-yellow-400 text-yellow-400")} />
{t('archive')} {isStarred ? t('tooltips.unstar') : t('tooltips.star')}
</button> </button>
{/* Move to folder (opens sub-view) */}
{moveTree.length > 0 && onMoveToMailbox && (
<button
onClick={() => setMoreMenuSub('move')}
className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
>
<FolderInput className="w-5 h-5" />
<span className="flex-1">{t('move_to')}</span>
<ChevronRight className="w-4 h-4 text-muted-foreground" />
</button>
)}
{/* Tag (opens sub-view) */} {/* Tag (opens sub-view) */}
{colorOptions.length > 0 && ( {colorOptions.length > 0 && (
<button <button
@@ -3637,28 +3622,6 @@ export function EmailViewer({
<ChevronRight className="w-4 h-4 text-muted-foreground" /> <ChevronRight className="w-4 h-4 text-muted-foreground" />
</button> </button>
)} )}
{/* Spam */}
{(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>
)}
{/* Toggle read state */}
<button
onClick={() => { onMarkAsRead?.(email.id, isUnread); 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"
>
{isUnread ? <MailOpen className="w-5 h-5" /> : <Mail className="w-5 h-5" />}
{isUnread ? t('mark_read') : t('mark_unread')}
</button>
<button <button
onClick={() => { handlePrint(); setMoreMenuOpen(false); }} 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" className="w-full px-4 py-3 min-h-[44px] text-sm text-left hover:bg-muted text-foreground flex items-center gap-3"
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Odebrat hvězdičku", "unstar": "Odebrat hvězdičku",
"mark_unread": "Označit jako nepřečtené", "mark_unread": "Označit jako nepřečtené",
"mark_read": "Označit jako přečtené", "mark_read": "Označit jako přečtené",
"unread": "Nepřečtené",
"read": "Přečtené",
"spam_short": "Spam",
"not_spam_short": "Není spam",
"move": "Přesunout",
"print": "Tisk", "print": "Tisk",
"view_source": "Zobrazit zdrojový kód", "view_source": "Zobrazit zdrojový kód",
"export_email": "Exportovat jako .eml", "export_email": "Exportovat jako .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Stern entfernen", "unstar": "Stern entfernen",
"mark_unread": "Als ungelesen markieren", "mark_unread": "Als ungelesen markieren",
"mark_read": "Als gelesen markieren", "mark_read": "Als gelesen markieren",
"unread": "Ungelesen",
"read": "Gelesen",
"spam_short": "Spam",
"not_spam_short": "Kein Spam",
"move": "Verschieben",
"print": "Drucken", "print": "Drucken",
"view_source": "Quelltext anzeigen", "view_source": "Quelltext anzeigen",
"export_email": "Als .eml exportieren", "export_email": "Als .eml exportieren",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Unstar", "unstar": "Unstar",
"mark_unread": "Mark as unread", "mark_unread": "Mark as unread",
"mark_read": "Mark as read", "mark_read": "Mark as read",
"unread": "Unread",
"read": "Read",
"spam_short": "Spam",
"not_spam_short": "Not spam",
"move": "Move",
"print": "Print", "print": "Print",
"view_source": "View source", "view_source": "View source",
"export_email": "Export as .eml", "export_email": "Export as .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Quitar destacado", "unstar": "Quitar destacado",
"mark_unread": "Marcar como no leído", "mark_unread": "Marcar como no leído",
"mark_read": "Marcar como leído", "mark_read": "Marcar como leído",
"unread": "No leído",
"read": "Leído",
"spam_short": "Spam",
"not_spam_short": "No spam",
"move": "Mover",
"print": "Imprimir", "print": "Imprimir",
"view_source": "Ver código fuente", "view_source": "Ver código fuente",
"export_email": "Exportar como .eml", "export_email": "Exportar como .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Retirer des favoris", "unstar": "Retirer des favoris",
"mark_unread": "Marquer comme non lu", "mark_unread": "Marquer comme non lu",
"mark_read": "Marquer comme lu", "mark_read": "Marquer comme lu",
"unread": "Non lu",
"read": "Lu",
"spam_short": "Spam",
"not_spam_short": "Non spam",
"move": "Déplacer",
"print": "Imprimer", "print": "Imprimer",
"view_source": "Voir la source", "view_source": "Voir la source",
"export_email": "Exporter en .eml", "export_email": "Exporter en .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Rimuovi stella", "unstar": "Rimuovi stella",
"mark_unread": "Segna come non letto", "mark_unread": "Segna come non letto",
"mark_read": "Segna come letto", "mark_read": "Segna come letto",
"unread": "Non letto",
"read": "Letto",
"spam_short": "Spam",
"not_spam_short": "Non spam",
"move": "Sposta",
"print": "Stampa", "print": "Stampa",
"view_source": "Visualizza sorgente", "view_source": "Visualizza sorgente",
"export_email": "Esporta come .eml", "export_email": "Esporta come .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "スターを外す", "unstar": "スターを外す",
"mark_unread": "未読にする", "mark_unread": "未読にする",
"mark_read": "既読にする", "mark_read": "既読にする",
"unread": "未読",
"read": "既読",
"spam_short": "迷惑メール",
"not_spam_short": "迷惑メールでない",
"move": "移動",
"print": "印刷", "print": "印刷",
"view_source": "ソースを表示", "view_source": "ソースを表示",
"export_email": ".emlとしてエクスポート", "export_email": ".emlとしてエクスポート",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "별표 해제", "unstar": "별표 해제",
"mark_unread": "읽지 않은 상태로 표시", "mark_unread": "읽지 않은 상태로 표시",
"mark_read": "읽은 상태로 표시", "mark_read": "읽은 상태로 표시",
"unread": "안 읽음",
"read": "읽음",
"spam_short": "스팸",
"not_spam_short": "스팸 아님",
"move": "이동",
"print": "인쇄", "print": "인쇄",
"view_source": "원본 보기", "view_source": "원본 보기",
"export_email": ".eml 파일로 내보내기", "export_email": ".eml 파일로 내보내기",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Noņemt zvaigznīti", "unstar": "Noņemt zvaigznīti",
"mark_unread": "Atzīmēt kā nelasītu", "mark_unread": "Atzīmēt kā nelasītu",
"mark_read": "Atzīmēt kā izlasītu", "mark_read": "Atzīmēt kā izlasītu",
"unread": "Nelasīts",
"read": "Lasīts",
"spam_short": "Spams",
"not_spam_short": "Nav spams",
"move": "Pārvietot",
"print": "Drukāt", "print": "Drukāt",
"view_source": "Skatīt avota kodu", "view_source": "Skatīt avota kodu",
"export_email": "Eksportēt kā .eml", "export_email": "Eksportēt kā .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Ster verwijderen", "unstar": "Ster verwijderen",
"mark_unread": "Markeren als ongelezen", "mark_unread": "Markeren als ongelezen",
"mark_read": "Markeren als gelezen", "mark_read": "Markeren als gelezen",
"unread": "Ongelezen",
"read": "Gelezen",
"spam_short": "Spam",
"not_spam_short": "Geen spam",
"move": "Verplaatsen",
"print": "Afdrukken", "print": "Afdrukken",
"view_source": "Bron bekijken", "view_source": "Bron bekijken",
"export_email": "Exporteren als .eml", "export_email": "Exporteren als .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Usuń gwiazdkę", "unstar": "Usuń gwiazdkę",
"mark_unread": "Oznacz jako nieprzeczytane", "mark_unread": "Oznacz jako nieprzeczytane",
"mark_read": "Oznacz jako przeczytane", "mark_read": "Oznacz jako przeczytane",
"unread": "Nieprzeczytane",
"read": "Przeczytane",
"spam_short": "Spam",
"not_spam_short": "Nie spam",
"move": "Przenieś",
"print": "Drukuj", "print": "Drukuj",
"view_source": "Pokaż źródło", "view_source": "Pokaż źródło",
"export_email": "Eksportuj jako .eml", "export_email": "Eksportuj jako .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Remover Estrela", "unstar": "Remover Estrela",
"mark_unread": "Marcar como não lido", "mark_unread": "Marcar como não lido",
"mark_read": "Marcar como lido", "mark_read": "Marcar como lido",
"unread": "Não lido",
"read": "Lido",
"spam_short": "Spam",
"not_spam_short": "Não spam",
"move": "Mover",
"print": "Imprimir", "print": "Imprimir",
"view_source": "Ver código-fonte", "view_source": "Ver código-fonte",
"export_email": "Exportar como .eml", "export_email": "Exportar como .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Снять пометку", "unstar": "Снять пометку",
"mark_unread": "Отметить непрочитанным", "mark_unread": "Отметить непрочитанным",
"mark_read": "Отметить прочитанным", "mark_read": "Отметить прочитанным",
"unread": "Не прочитано",
"read": "Прочитано",
"spam_short": "Спам",
"not_spam_short": "Не спам",
"move": "Переместить",
"print": "Распечатать", "print": "Распечатать",
"view_source": "Просмотреть исходный код", "view_source": "Просмотреть исходный код",
"export_email": "Экспортировать как .eml", "export_email": "Экспортировать как .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Yıldızı Kaldır", "unstar": "Yıldızı Kaldır",
"mark_unread": "Okunmadı olarak işaretle", "mark_unread": "Okunmadı olarak işaretle",
"mark_read": "Okundu olarak işaretle", "mark_read": "Okundu olarak işaretle",
"unread": "Okunmadı",
"read": "Okundu",
"spam_short": "Spam",
"not_spam_short": "Spam değil",
"move": "Taşı",
"print": "Yazdır", "print": "Yazdır",
"view_source": "Kaynağı görüntüle", "view_source": "Kaynağı görüntüle",
"export_email": ".eml olarak dışa aktar", "export_email": ".eml olarak dışa aktar",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "Зняти зірочку", "unstar": "Зняти зірочку",
"mark_unread": "Позначити як непрочитане", "mark_unread": "Позначити як непрочитане",
"mark_read": "Позначити як прочитане", "mark_read": "Позначити як прочитане",
"unread": "Непрочитане",
"read": "Прочитане",
"spam_short": "Спам",
"not_spam_short": "Не спам",
"move": "Перенести",
"print": "Роздрукувати", "print": "Роздрукувати",
"view_source": "Переглянути джерело", "view_source": "Переглянути джерело",
"export_email": "Експортувати як .eml", "export_email": "Експортувати як .eml",
+5
View File
@@ -223,6 +223,11 @@
"unstar": "取消星标", "unstar": "取消星标",
"mark_unread": "标记为未读", "mark_unread": "标记为未读",
"mark_read": "标记为已读", "mark_read": "标记为已读",
"unread": "未读",
"read": "已读",
"spam_short": "垃圾邮件",
"not_spam_short": "非垃圾邮件",
"move": "移动",
"print": "打印", "print": "打印",
"view_source": "查看源码", "view_source": "查看源码",
"export_email": "导出为 .eml", "export_email": "导出为 .eml",