feat: mobile-responsive mail plugin UI
This commit is contained in:
@@ -26,9 +26,9 @@ function ToolbarSearch({ item }: { item: ToolbarItem }) {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder={item.searchPlaceholder || 'Suchen...'}
|
placeholder={item.searchPlaceholder || 'Suchen...'}
|
||||||
onChange={(e) => item.onSearch?.(e.target.value)}
|
onChange={(e) => item.onSearch?.(e.target.value)}
|
||||||
className="w-40 px-2 py-1 text-xs border border-secondary-200 rounded
|
className="w-28 sm:w-40 px-2 py-1 text-xs border border-secondary-200 rounded
|
||||||
focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300
|
focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300
|
||||||
placeholder:text-secondary-400"
|
placeholder:text-secondary-400 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -38,9 +38,9 @@ function ToolbarSelect({ item }: { item: ToolbarItem }) {
|
|||||||
<select
|
<select
|
||||||
value={item.selectValue || ''}
|
value={item.selectValue || ''}
|
||||||
onChange={(e) => item.onSelect?.(e.target.value)}
|
onChange={(e) => item.onSelect?.(e.target.value)}
|
||||||
className="px-2 py-1 text-xs border border-secondary-200 rounded
|
className="px-1.5 sm:px-2 py-1 text-xs border border-secondary-200 rounded
|
||||||
focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300
|
focus:outline-none focus:border-primary-400 focus:ring-1 focus:ring-primary-300
|
||||||
bg-white cursor-pointer"
|
bg-white cursor-pointer flex-shrink-0 max-w-[120px] sm:max-w-none"
|
||||||
>
|
>
|
||||||
{item.selectOptions?.map((opt) => (
|
{item.selectOptions?.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
@@ -66,7 +66,7 @@ export function PluginToolbar() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-1 px-3 py-1 bg-white border-b border-secondary-200 min-h-[36px]"
|
className="flex items-center gap-1 px-2 sm:px-3 py-1 bg-white border-b border-secondary-200 min-h-[36px] overflow-x-auto"
|
||||||
data-testid="plugin-toolbar"
|
data-testid="plugin-toolbar"
|
||||||
>
|
>
|
||||||
{groupNames.map((groupName, gi) => (
|
{groupNames.map((groupName, gi) => (
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function ComposeModal({
|
|||||||
const title = mode === 'reply' ? t('mail.reply') : mode === 'forward' ? t('mail.forward') : t('mail.compose');
|
const title = mode === 'reply' ? t('mail.reply') : mode === 'forward' ? t('mail.forward') : t('mail.compose');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={open} onClose={onClose} title={title} size="xl" closeOnBackdrop={false}>
|
<Modal open={open} onClose={onClose} title={title} size="xl" closeOnBackdrop={false} fullScreenMobile>
|
||||||
<div className="space-y-4" data-testid="compose-modal">
|
<div className="space-y-4" data-testid="compose-modal">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center gap-1 border-b border-secondary-200 pb-2" data-testid="compose-toolbar">
|
<div className="flex items-center gap-1 border-b border-secondary-200 pb-2" data-testid="compose-toolbar">
|
||||||
@@ -225,7 +225,7 @@ export function ComposeModal({
|
|||||||
{t('mail.showCcBcc')}
|
{t('mail.showCcBcc')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-2">
|
<div className="w-full grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
<Input
|
<Input
|
||||||
label={t('mail.cc')}
|
label={t('mail.cc')}
|
||||||
value={cc}
|
value={cc}
|
||||||
@@ -278,7 +278,7 @@ export function ComposeModal({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex justify-end gap-2 pt-2 border-t border-secondary-200">
|
<div className="flex justify-end gap-2 pt-2 border-t border-secondary-200 sticky bottom-0 bg-white py-3">
|
||||||
<Button variant="secondary" onClick={onClose} type="button">
|
<Button variant="secondary" onClick={onClose} type="button">
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export function MailDetail({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full" data-testid="mail-detail">
|
<div className="flex flex-col h-full" data-testid="mail-detail">
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center gap-2 px-4 py-3 border-b border-secondary-200" data-testid="mail-detail-toolbar">
|
<div className="flex items-center gap-1 sm:gap-2 px-3 py-2 md:px-4 md:py-3 border-b border-secondary-200 overflow-x-auto" data-testid="mail-detail-toolbar">
|
||||||
<Button variant="secondary" size="sm" onClick={() => onReply(mail)} icon={
|
<Button variant="secondary" size="sm" onClick={() => onReply(mail)} icon={
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||||
@@ -135,9 +135,9 @@ export function MailDetail({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Headers */}
|
{/* Headers */}
|
||||||
<div className="px-4 py-3 border-b border-secondary-200" data-testid="mail-detail-headers">
|
<div className="px-3 py-2 md:px-4 md:py-3 border-b border-secondary-200" data-testid="mail-detail-headers">
|
||||||
<div className="flex items-start justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2 sm:gap-4">
|
||||||
<h2 className="text-lg font-semibold text-secondary-900 flex-1">{mail.subject || t('mail.noSubject')}</h2>
|
<h2 className="text-base md:text-lg font-semibold text-secondary-900 flex-1">{mail.subject || t('mail.noSubject')}</h2>
|
||||||
{mail.labels && mail.labels.length > 0 && (
|
{mail.labels && mail.labels.length > 0 && (
|
||||||
<div className="flex gap-1 flex-shrink-0">
|
<div className="flex gap-1 flex-shrink-0">
|
||||||
{mail.labels.map((label) => (
|
{mail.labels.map((label) => (
|
||||||
@@ -148,30 +148,30 @@ export function MailDetail({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 space-y-1 text-sm text-secondary-600">
|
<div className="mt-2 space-y-1 text-xs md:text-sm text-secondary-600">
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col sm:flex-row sm:gap-2">
|
||||||
<span className="font-medium text-secondary-500">{t('mail.from')}:</span>
|
<span className="font-medium text-secondary-500 flex-shrink-0">{t('mail.from')}:</span>
|
||||||
<span>{mail.from_name ? `${mail.from_name} <${mail.from_address}>` : mail.from_address}</span>
|
<span className="break-words">{mail.from_name ? `${mail.from_name} <${mail.from_address}>` : mail.from_address}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col sm:flex-row sm:gap-2">
|
||||||
<span className="font-medium text-secondary-500">{t('mail.to')}:</span>
|
<span className="font-medium text-secondary-500 flex-shrink-0">{t('mail.to')}:</span>
|
||||||
<span>{mail.to_addresses.join(', ')}</span>
|
<span className="break-words">{mail.to_addresses.join(', ')}</span>
|
||||||
</div>
|
</div>
|
||||||
{mail.cc_addresses.length > 0 && (
|
{mail.cc_addresses.length > 0 && (
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col sm:flex-row sm:gap-2">
|
||||||
<span className="font-medium text-secondary-500">{t('mail.cc')}:</span>
|
<span className="font-medium text-secondary-500 flex-shrink-0">{t('mail.cc')}:</span>
|
||||||
<span>{mail.cc_addresses.join(', ')}</span>
|
<span className="break-words">{mail.cc_addresses.join(', ')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex gap-2">
|
<div className="flex flex-col sm:flex-row sm:gap-2">
|
||||||
<span className="font-medium text-secondary-500">{t('mail.date')}:</span>
|
<span className="font-medium text-secondary-500 flex-shrink-0">{t('mail.date')}:</span>
|
||||||
<span>{formatFullDate(mail.date)}</span>
|
<span>{formatFullDate(mail.date)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div className="flex-1 overflow-y-auto px-4 py-4" data-testid="mail-detail-body">
|
<div className="flex-1 overflow-y-auto px-3 py-3 md:px-4 md:py-4" data-testid="mail-detail-body">
|
||||||
{safeHtml && isSafe ? (
|
{safeHtml && isSafe ? (
|
||||||
<div
|
<div
|
||||||
className="prose prose-sm max-w-none text-secondary-800"
|
className="prose prose-sm max-w-none text-secondary-800"
|
||||||
@@ -190,9 +190,9 @@ export function MailDetail({
|
|||||||
|
|
||||||
{/* Attachments */}
|
{/* Attachments */}
|
||||||
{mail.attachments && mail.attachments.length > 0 && (
|
{mail.attachments && mail.attachments.length > 0 && (
|
||||||
<div className="px-4 py-3 border-t border-secondary-200" data-testid="mail-detail-attachments">
|
<div className="px-3 py-2 md:px-4 md:py-3 border-t border-secondary-200" data-testid="mail-detail-attachments">
|
||||||
<h3 className="text-sm font-semibold text-secondary-700 mb-2">{t('mail.attachments')}</h3>
|
<h3 className="text-sm font-semibold text-secondary-700 mb-2">{t('mail.attachments')}</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-1 md:space-y-2">
|
||||||
{mail.attachments.map((att) => (
|
{mail.attachments.map((att) => (
|
||||||
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md hover:bg-secondary-50 min-h-touch">
|
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md hover:bg-secondary-50 min-h-touch">
|
||||||
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function FolderNode({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onSelect(folder.id)}
|
onClick={() => onSelect(folder.id)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'flex-1 flex items-center gap-2 px-2 py-1.5 rounded-md text-sm min-h-touch',
|
'flex-1 flex items-center gap-2 px-2 py-2 md:py-1.5 rounded-md text-sm min-h-touch',
|
||||||
'motion-safe:transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
'motion-safe:transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
||||||
isSelected
|
isSelected
|
||||||
? 'bg-primary-50 text-primary-700 font-medium'
|
? 'bg-primary-50 text-primary-700 font-medium'
|
||||||
@@ -203,7 +203,7 @@ function AccountNode({
|
|||||||
<div role="treeitem" aria-label={account.email}>
|
<div role="treeitem" aria-label={account.email}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setExpanded(!expanded)}
|
onClick={() => setExpanded(!expanded)}
|
||||||
className="w-full flex items-center gap-1 px-1 py-1.5 rounded-md text-sm font-semibold text-secondary-900 hover:bg-secondary-50"
|
className="w-full flex items-center gap-1 px-1 py-2 md:py-1.5 rounded-md text-sm font-semibold text-secondary-900 hover:bg-secondary-50 min-h-touch"
|
||||||
data-testid={`account-${account.id}`}
|
data-testid={`account-${account.id}`}
|
||||||
>
|
>
|
||||||
<ChevronIcon expanded={expanded} />
|
<ChevronIcon expanded={expanded} />
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function MailList({
|
|||||||
<button
|
<button
|
||||||
onClick={() => onSelectMail(mail)}
|
onClick={() => onSelectMail(mail)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
'w-full text-left px-4 py-3 hover:bg-secondary-50 min-h-touch motion-safe:transition-colors',
|
'w-full text-left px-3 py-2 md:px-4 md:py-3 hover:bg-secondary-50 min-h-touch motion-safe:transition-colors',
|
||||||
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
||||||
mail.id === selectedMailId && 'bg-primary-50',
|
mail.id === selectedMailId && 'bg-primary-50',
|
||||||
!mail.is_seen && 'font-semibold',
|
!mail.is_seen && 'font-semibold',
|
||||||
@@ -102,7 +102,7 @@ export function MailList({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span>
|
<span className="text-xs text-secondary-400 flex-shrink-0">{formatDate(mail.date)}</span>
|
||||||
</div>
|
</div>
|
||||||
<p className={clsx('text-sm truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}>
|
<p className={clsx('text-xs md:text-sm truncate mt-0.5', !mail.is_seen ? 'text-secondary-800' : 'text-secondary-600')}>
|
||||||
{mail.subject || t('mail.noSubject')}
|
{mail.subject || t('mail.noSubject')}
|
||||||
</p>
|
</p>
|
||||||
{mail.labels && mail.labels.length > 0 && (
|
{mail.labels && mail.labels.length > 0 && (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface ModalProps {
|
|||||||
closeOnBackdrop?: boolean;
|
closeOnBackdrop?: boolean;
|
||||||
closeOnEscape?: boolean;
|
closeOnEscape?: boolean;
|
||||||
showCloseButton?: boolean;
|
showCloseButton?: boolean;
|
||||||
|
fullScreenMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
@@ -28,6 +29,7 @@ export function Modal({
|
|||||||
closeOnBackdrop = true,
|
closeOnBackdrop = true,
|
||||||
closeOnEscape = true,
|
closeOnEscape = true,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
|
fullScreenMobile = false,
|
||||||
}: ModalProps) {
|
}: ModalProps) {
|
||||||
const dialogRef = useRef<HTMLDivElement>(null);
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
const previouslyFocused = useRef<HTMLElement | null>(null);
|
const previouslyFocused = useRef<HTMLElement | null>(null);
|
||||||
@@ -60,7 +62,10 @@ export function Modal({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
className={clsx(
|
||||||
|
'fixed inset-0 z-50 flex items-center justify-center',
|
||||||
|
fullScreenMobile ? 'p-0 md:p-4' : 'p-4'
|
||||||
|
)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby={title ? 'modal-title' : undefined}
|
aria-labelledby={title ? 'modal-title' : undefined}
|
||||||
@@ -76,12 +81,14 @@ export function Modal({
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'relative bg-white rounded-lg shadow-xl w-full',
|
'relative bg-white rounded-lg shadow-xl w-full',
|
||||||
'motion-safe:animate-scale',
|
'motion-safe:animate-scale',
|
||||||
|
fullScreenMobile && 'h-full md:h-auto md:max-h-[85vh]',
|
||||||
|
fullScreenMobile ? 'rounded-none md:rounded-lg' : '',
|
||||||
sizeClasses[size]
|
sizeClasses[size]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{title && (
|
{title && (
|
||||||
<div className="px-6 py-4 border-b border-secondary-200">
|
<div className="px-4 py-3 md:px-6 md:py-4 border-b border-secondary-200">
|
||||||
<h2 id="modal-title" className="text-lg font-semibold text-secondary-900">
|
<h2 id="modal-title" className="text-base md:text-lg font-semibold text-secondary-900">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +96,7 @@ export function Modal({
|
|||||||
{showCloseButton && (
|
{showCloseButton && (
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-4 right-4 text-secondary-400 hover:text-secondary-600 min-h-touch min-w-touch flex items-center justify-center rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
className="absolute top-3 right-3 md:top-4 md:right-4 text-secondary-400 hover:text-secondary-600 min-h-touch min-w-touch flex items-center justify-center rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
||||||
aria-label="Close dialog"
|
aria-label="Close dialog"
|
||||||
>
|
>
|
||||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
@@ -97,7 +104,10 @@ export function Modal({
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<div className="px-6 py-4 max-h-[70vh] overflow-y-auto">
|
<div className={clsx(
|
||||||
|
'px-4 py-4 md:px-6 overflow-y-auto',
|
||||||
|
fullScreenMobile ? 'max-h-none md:max-h-[70vh] h-full md:h-auto' : 'max-h-[70vh]'
|
||||||
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export function MailPage() {
|
|||||||
const [forwardMailState, setForwardMailState] = useState<Mail | null>(null);
|
const [forwardMailState, setForwardMailState] = useState<Mail | null>(null);
|
||||||
const [downloadingAttachmentId, setDownloadingAttachmentId] = useState<string | null>(null);
|
const [downloadingAttachmentId, setDownloadingAttachmentId] = useState<string | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [activeView, setActiveView] = useState<'folders' | 'list' | 'detail'>('folders');
|
||||||
|
|
||||||
// Load accounts
|
// Load accounts
|
||||||
const loadAccounts = useCallback(async () => {
|
const loadAccounts = useCallback(async () => {
|
||||||
@@ -168,6 +169,7 @@ export function MailPage() {
|
|||||||
setMailsPage(1);
|
setMailsPage(1);
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSelectedMail(null);
|
setSelectedMail(null);
|
||||||
|
setActiveView('list');
|
||||||
},
|
},
|
||||||
[folders],
|
[folders],
|
||||||
);
|
);
|
||||||
@@ -175,6 +177,7 @@ export function MailPage() {
|
|||||||
// Handle mail selection
|
// Handle mail selection
|
||||||
const handleSelectMail = useCallback(
|
const handleSelectMail = useCallback(
|
||||||
async (mail: Mail) => {
|
async (mail: Mail) => {
|
||||||
|
setActiveView('detail');
|
||||||
setLoadingMail(true);
|
setLoadingMail(true);
|
||||||
try {
|
try {
|
||||||
const detail = await getMail(mail.id);
|
const detail = await getMail(mail.id);
|
||||||
@@ -435,8 +438,8 @@ export function MailPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Three-pane layout with resizable panels */}
|
{/* Desktop: three-pane layout with resizable panels */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="hidden md:flex flex-1 overflow-hidden">
|
||||||
{/* Folder tree — resizable */}
|
{/* Folder tree — resizable */}
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
initialWidth={224}
|
initialWidth={224}
|
||||||
@@ -495,6 +498,82 @@ export function MailPage() {
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile: single-pane view switching */}
|
||||||
|
<div className="flex md:hidden flex-1 overflow-hidden flex-col">
|
||||||
|
{/* View 1: Folder tree */}
|
||||||
|
{activeView === 'folders' && (
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white" data-testid="mobile-folder-pane">
|
||||||
|
<div className="p-3">
|
||||||
|
<MailFolderTree
|
||||||
|
accounts={accounts}
|
||||||
|
folders={folders}
|
||||||
|
selectedFolderId={selectedFolderId}
|
||||||
|
onSelect={handleSelectFolder}
|
||||||
|
loading={loadingFolders}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View 2: Mail list */}
|
||||||
|
{activeView === 'list' && (
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white" data-testid="mobile-list-pane">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-secondary-200 sticky top-0 bg-white z-10">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('folders')}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm text-secondary-700 hover:bg-secondary-100 min-h-touch"
|
||||||
|
aria-label="Zurück zu Ordnern"
|
||||||
|
data-testid="mobile-back-to-folders"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium">Ordner</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<MailList
|
||||||
|
mails={mails}
|
||||||
|
selectedMailId={selectedMail?.id || null}
|
||||||
|
onSelectMail={handleSelectMail}
|
||||||
|
loading={loadingMails}
|
||||||
|
currentPage={mailsPage}
|
||||||
|
total={mailsTotal}
|
||||||
|
pageSize={PAGE_SIZE}
|
||||||
|
onPageChange={setMailsPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View 3: Mail detail */}
|
||||||
|
{activeView === 'detail' && (
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white" data-testid="mobile-detail-pane">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-secondary-200 sticky top-0 bg-white z-10">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('list')}
|
||||||
|
className="inline-flex items-center gap-1 px-2 py-1 rounded text-sm text-secondary-700 hover:bg-secondary-100 min-h-touch"
|
||||||
|
aria-label="Zurück zur Liste"
|
||||||
|
data-testid="mobile-back-to-list"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-sm font-medium">Zurück</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<MailDetail
|
||||||
|
mail={selectedMail}
|
||||||
|
loading={loadingMail}
|
||||||
|
onReply={handleReply}
|
||||||
|
onForward={handleForward}
|
||||||
|
onCreateEvent={handleCreateEvent}
|
||||||
|
onToggleFlag={handleToggleFlag}
|
||||||
|
onDownloadAttachment={handleDownloadAttachment}
|
||||||
|
downloadingAttachmentId={downloadingAttachmentId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<ComposeModal
|
<ComposeModal
|
||||||
open={composeOpen}
|
open={composeOpen}
|
||||||
mode={composeMode}
|
mode={composeMode}
|
||||||
|
|||||||
Reference in New Issue
Block a user