feat: improve frontend accessibility, dark mode, and UX
Build and Push Multi-Platform Images / build-and-push (push) Successful in 18s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 18s
- Replace div with semantic button elements in AppLayout for accessibility - Use Ant Design theme tokens instead of hardcoded colors in MailDetailPage - Add loading state to refresh buttons and pagination info - Add German empty state messages to tables - Clean up unnecessary ConfigProvider wrappers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -125,7 +125,7 @@ export default function AppLayout() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Logo / Collapse-Toggle */}
|
{/* Logo / Collapse-Toggle */}
|
||||||
<div
|
<button
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
style={{
|
style={{
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -134,7 +134,11 @@ export default function AppLayout() {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
|
border: 'none',
|
||||||
borderBottom: `1px solid ${isDark ? 'rgba(255,255,255,0.08)' : '#e2e4ea'}`,
|
borderBottom: `1px solid ${isDark ? 'rgba(255,255,255,0.08)' : '#e2e4ea'}`,
|
||||||
|
background: 'transparent',
|
||||||
|
width: '100%',
|
||||||
|
padding: 0,
|
||||||
transition: 'background 0.2s',
|
transition: 'background 0.2s',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.02)')}
|
onMouseEnter={(e) => (e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.02)')}
|
||||||
@@ -143,7 +147,7 @@ export default function AppLayout() {
|
|||||||
<Text strong style={{ color: logoColor, fontSize: collapsed ? 14 : 18, transition: 'font-size 0.2s' }}>
|
<Text strong style={{ color: logoColor, fontSize: collapsed ? 14 : 18, transition: 'font-size 0.2s' }}>
|
||||||
{collapsed ? 'PM' : 'Paperless'}
|
{collapsed ? 'PM' : 'Paperless'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{/* Navigation Menu */}
|
{/* Navigation Menu */}
|
||||||
<Menu
|
<Menu
|
||||||
@@ -179,7 +183,7 @@ export default function AppLayout() {
|
|||||||
>
|
>
|
||||||
{/* Theme Toggle */}
|
{/* Theme Toggle */}
|
||||||
<Tooltip title={isDark ? 'Light Mode' : 'Dark Mode'} placement="right">
|
<Tooltip title={isDark ? 'Light Mode' : 'Dark Mode'} placement="right">
|
||||||
<div
|
<button
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -190,14 +194,17 @@ export default function AppLayout() {
|
|||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: subtleColor,
|
color: subtleColor,
|
||||||
justifyContent: collapsed ? 'center' : 'flex-start',
|
justifyContent: collapsed ? 'center' : 'flex-start',
|
||||||
transition: 'all 0.2s',
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
width: '100%',
|
||||||
|
transition: 'background 0.2s',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.08)' : '#eef1f8')}
|
onMouseEnter={(e) => (e.currentTarget.style.background = isDark ? 'rgba(255,255,255,0.08)' : '#eef1f8')}
|
||||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||||
>
|
>
|
||||||
{isDark ? <SunOutlined style={{ fontSize: 16 }} /> : <MoonOutlined style={{ fontSize: 16 }} />}
|
{isDark ? <SunOutlined style={{ fontSize: 16 }} /> : <MoonOutlined style={{ fontSize: 16 }} />}
|
||||||
{!collapsed && <Text style={{ color: subtleColor, fontSize: 13 }}>{isDark ? 'Light Mode' : 'Dark Mode'}</Text>}
|
{!collapsed && <Text style={{ color: subtleColor, fontSize: 13 }}>{isDark ? 'Light Mode' : 'Dark Mode'}</Text>}
|
||||||
</div>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
{/* User Menu */}
|
{/* User Menu */}
|
||||||
|
|||||||
@@ -1176,7 +1176,7 @@ export default function InboxDetailPage() {
|
|||||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/inbox')}>
|
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/inbox')}>
|
||||||
Zurück
|
Zurück
|
||||||
</Button>
|
</Button>
|
||||||
<Title level={4} style={{ margin: 0 }}>
|
<Title level={3} style={{ margin: 0 }}>
|
||||||
{file.name}
|
{file.name}
|
||||||
</Title>
|
</Title>
|
||||||
<SourceTag source={file.source} />
|
<SourceTag source={file.source} />
|
||||||
|
|||||||
@@ -387,7 +387,7 @@ export default function InboxPage() {
|
|||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>
|
||||||
Aktualisieren
|
Aktualisieren
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Card, Button, Space, Spin, Tag, Typography, Table, message, Empty, Popconfirm
|
Card, Button, Space, Spin, Tag, Typography, Table, message, Empty, Popconfirm, theme
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { ArrowLeftOutlined, FileTextOutlined, CloseCircleOutlined, LinkOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, FileTextOutlined, CloseCircleOutlined, LinkOutlined } from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
@@ -23,6 +23,7 @@ export default function MailDetailPage() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [previewLoading, setPreviewLoading] = useState(false);
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
const [wizardOpen, setWizardOpen] = useState(false);
|
const [wizardOpen, setWizardOpen] = useState(false);
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -208,7 +209,7 @@ export default function MailDetailPage() {
|
|||||||
|
|
||||||
{/* Rechte Seite: Anhänge + Vorschau */}
|
{/* Rechte Seite: Anhänge + Vorschau */}
|
||||||
<Card size="small" styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 200px)' } }}>
|
<Card size="small" styles={{ body: { padding: 0, display: 'flex', flexDirection: 'column', height: 'calc(100vh - 200px)' } }}>
|
||||||
<div style={{ flex: '0 0 auto', borderBottom: '1px solid #303030', maxHeight: 240, overflow: 'auto' }}>
|
<div style={{ flex: '0 0 auto', borderBottom: `1px solid ${token.colorBorder}`, maxHeight: 240, overflow: 'auto' }}>
|
||||||
<Table<EmailAttachment>
|
<Table<EmailAttachment>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={attachments}
|
dataSource={attachments}
|
||||||
@@ -223,7 +224,7 @@ export default function MailDetailPage() {
|
|||||||
locale={{ emptyText: <Empty description="Keine Anhänge" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
locale={{ emptyText: <Empty description="Keine Anhänge" image={Empty.PRESENTED_IMAGE_SIMPLE} /> }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minHeight: 0, background: '#1a1a2e' }}>
|
<div style={{ flex: 1, minHeight: 0, background: token.colorBgLayout }}>
|
||||||
{previewLoading ? (
|
{previewLoading ? (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
|
||||||
<Spin />
|
<Spin />
|
||||||
@@ -240,7 +241,7 @@ export default function MailDetailPage() {
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#888' }}>
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%', color: '#888' }}>
|
||||||
<Space vertical align="center">
|
<Space direction="vertical" align="center">
|
||||||
<FileTextOutlined style={{ fontSize: 48 }} />
|
<FileTextOutlined style={{ fontSize: 48 }} />
|
||||||
<Text type="secondary">Kein Anhang ausgewählt</Text>
|
<Text type="secondary">Kein Anhang ausgewählt</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ export default function MailpostfachPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
rowKey="Id"
|
rowKey="Id"
|
||||||
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} E-Mails` }}
|
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} E-Mails` }}
|
||||||
|
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||||
onRow={(record) => ({
|
onRow={(record) => ({
|
||||||
onClick: () => navigate(`/mailpostfach/${record.Id}`),
|
onClick: () => navigate(`/mailpostfach/${record.Id}`),
|
||||||
style: { cursor: 'pointer' },
|
style: { cursor: 'pointer' },
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Table, Popover, Button, Space, message, ConfigProvider, Tooltip } from 'antd';
|
import { Table, Popover, Button, Space, message, Tooltip, Typography } from 'antd';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { posteingangApi } from '../api/posteingang';
|
import { posteingangApi } from '../api/posteingang';
|
||||||
@@ -97,7 +99,7 @@ export default function ManuellBearbeitenPage() {
|
|||||||
render: (text: string) => dayjs(text).format('DD.MM.YYYY HH:mm'),
|
render: (text: string) => dayjs(text).format('DD.MM.YYYY HH:mm'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Aktion',
|
title: 'Aktionen',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (_: any, record: PosteingangDocument) => (
|
render: (_: any, record: PosteingangDocument) => (
|
||||||
@@ -109,10 +111,9 @@ export default function ManuellBearbeitenPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider>
|
<div>
|
||||||
<div style={{ padding: '0 24px 24px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<Title level={3} style={{ margin: 0 }}>Manuell bearbeiten</Title>
|
||||||
<h2>Manuell bearbeiten</h2>
|
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip title="Manuell aktualisieren">
|
<Tooltip title="Manuell aktualisieren">
|
||||||
<Button icon={<ReloadOutlined />} onClick={fetchData} loading={loading} />
|
<Button icon={<ReloadOutlined />} onClick={fetchData} loading={loading} />
|
||||||
@@ -124,9 +125,10 @@ export default function ManuellBearbeitenPage() {
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 20 }}
|
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} Dokumente` }}
|
||||||
|
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentEditModal
|
<DocumentEditModal
|
||||||
documentId={selectedDoc?.id || null}
|
documentId={selectedDoc?.id || null}
|
||||||
document={selectedDoc}
|
document={selectedDoc}
|
||||||
@@ -136,7 +138,6 @@ export default function ManuellBearbeitenPage() {
|
|||||||
isPosteingang={false}
|
isPosteingang={false}
|
||||||
hasNextDocument={data.length > 1}
|
hasNextDocument={data.length > 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Table, Popover, Button, Space, message, ConfigProvider, Tooltip } from 'antd';
|
import { Table, Popover, Button, Space, message, Tooltip, Typography } from 'antd';
|
||||||
import { AuthImage } from '../utils/auth-resource';
|
import { AuthImage } from '../utils/auth-resource';
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const { Title } = Typography;
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { posteingangApi } from '../api/posteingang';
|
import { posteingangApi } from '../api/posteingang';
|
||||||
import type { PosteingangDocument } from '../api/posteingang';
|
import type { PosteingangDocument } from '../api/posteingang';
|
||||||
@@ -98,7 +100,7 @@ export default function PosteingangPage() {
|
|||||||
render: (text: string) => dayjs(text).format('DD.MM.YYYY HH:mm'),
|
render: (text: string) => dayjs(text).format('DD.MM.YYYY HH:mm'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Aktion',
|
title: 'Aktionen',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 150,
|
width: 150,
|
||||||
render: (_: any, record: PosteingangDocument) => (
|
render: (_: any, record: PosteingangDocument) => (
|
||||||
@@ -110,10 +112,9 @@ export default function PosteingangPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider>
|
<div>
|
||||||
<div style={{ padding: '0 24px 24px' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
<Title level={3} style={{ margin: 0 }}>Posteingang</Title>
|
||||||
<h2>Posteingang</h2>
|
|
||||||
<Space>
|
<Space>
|
||||||
<Tooltip title="Manuell aktualisieren">
|
<Tooltip title="Manuell aktualisieren">
|
||||||
<Button icon={<ReloadOutlined />} onClick={fetchData} loading={loading} />
|
<Button icon={<ReloadOutlined />} onClick={fetchData} loading={loading} />
|
||||||
@@ -125,9 +126,10 @@ export default function PosteingangPage() {
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 20 }}
|
pagination={{ pageSize: 20, showSizeChanger: true, showTotal: (t) => `${t} Dokumente` }}
|
||||||
|
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentEditModal
|
<DocumentEditModal
|
||||||
documentId={selectedDoc?.id || null}
|
documentId={selectedDoc?.id || null}
|
||||||
document={selectedDoc}
|
document={selectedDoc}
|
||||||
@@ -137,7 +139,6 @@ export default function PosteingangPage() {
|
|||||||
isPosteingang={true}
|
isPosteingang={true}
|
||||||
hasNextDocument={data.length > 1}
|
hasNextDocument={data.length > 1}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export default function TaskLogPage() {
|
|||||||
rowKey="TaskId"
|
rowKey="TaskId"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ pageSize: 20 }}
|
pagination={{ pageSize: 20 }}
|
||||||
|
locale={{ emptyText: 'Keine Einträge vorhanden' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user