Phase 0.2: migrate remaining SVGs to lucide-react icons

This commit is contained in:
Agent Zero
2026-07-23 03:21:05 +02:00
parent e9a5eee524
commit 4f8cda1566
41 changed files with 228 additions and 564 deletions
@@ -20,6 +20,7 @@ import {
} from '@/api/calendar'; } from '@/api/calendar';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Calendar as CalendarIcon, Pencil, Trash2, X } from 'lucide-react';
function formatDate(dateStr: string | null | undefined): string { function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '-'; if (!dateStr) return '-';
@@ -139,9 +140,7 @@ export function CalendarDetail({
className="flex flex-col items-center justify-center h-full p-6 text-center" className="flex flex-col items-center justify-center h-full p-6 text-center"
data-testid="calendar-detail-empty" data-testid="calendar-detail-empty"
> >
<svg className="w-12 h-12 text-secondary-300 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <CalendarIcon className="w-12 h-12 text-secondary-300 mb-3" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-sm font-medium text-secondary-600">{t('calendar.detail.noSelection')}</p> <p className="text-sm font-medium text-secondary-600">{t('calendar.detail.noSelection')}</p>
<p className="mt-1 text-xs text-secondary-400">{t('calendar.detail.noSelectionHint')}</p> <p className="mt-1 text-xs text-secondary-400">{t('calendar.detail.noSelectionHint')}</p>
</div> </div>
@@ -163,9 +162,7 @@ export function CalendarDetail({
aria-label={t('common.close')} aria-label={t('common.close')}
data-testid="calendar-detail-close" data-testid="calendar-detail-close"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
@@ -196,9 +193,7 @@ export function CalendarDetail({
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-primary-50 text-primary-700 hover:bg-primary-100 min-h-touch" className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-primary-50 text-primary-700 hover:bg-primary-100 min-h-touch"
data-testid="calendar-detail-edit" data-testid="calendar-detail-edit"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Pencil className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
{t('calendar.detail.edit')} {t('calendar.detail.edit')}
</button> </button>
<button <button
@@ -206,9 +201,7 @@ export function CalendarDetail({
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-danger-50 text-danger-700 hover:bg-danger-100 min-h-touch" className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-danger-50 text-danger-700 hover:bg-danger-100 min-h-touch"
data-testid="calendar-detail-delete" data-testid="calendar-detail-delete"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
{t('calendar.detail.delete')} {t('calendar.detail.delete')}
</button> </button>
</div> </div>
@@ -10,6 +10,7 @@ import React, { useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { Calendar, CalendarType } from '@/api/calendar'; import type { Calendar, CalendarType } from '@/api/calendar';
import { ChevronDown, Plus } from 'lucide-react';
const TYPE_ORDER: CalendarType[] = ['personal', 'team', 'project', 'company']; const TYPE_ORDER: CalendarType[] = ['personal', 'team', 'project', 'company'];
@@ -115,9 +116,7 @@ function TypeSection({
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600" className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
aria-label={expanded ? 'Collapse' : 'Expand'} aria-label={expanded ? 'Collapse' : 'Expand'}
> >
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
</svg>
</button> </button>
<span className="truncate text-secondary-700">{t(`calendar.type.${type}`)}</span> <span className="truncate text-secondary-700">{t(`calendar.type.${type}`)}</span>
<span className="ml-auto text-xs text-secondary-400">{calendars.length}</span> <span className="ml-auto text-xs text-secondary-400">{calendars.length}</span>
@@ -218,9 +217,7 @@ export function CalendarTree({
)} )}
data-testid="calendar-tree-new" data-testid="calendar-tree-new"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Plus className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{t('calendar.tree.newCalendar')} {t('calendar.tree.newCalendar')}
</button> </button>
</div> </div>
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import type { MessageBlock } from '@/store/commStore'; import type { MessageBlock } from '@/store/commStore';
import { ChevronRight } from 'lucide-react';
interface ContactCardBlockProps { interface ContactCardBlockProps {
block: MessageBlock; block: MessageBlock;
@@ -31,20 +32,7 @@ const ContactCardBlock: React.FC<ContactCardBlockProps> = ({ block }) => {
<p className="text-sm font-medium text-secondary-700 truncate">{contactName}</p> <p className="text-sm font-medium text-secondary-700 truncate">{contactName}</p>
<p className="text-xs text-secondary-400">Kontakt anzeigen</p> <p className="text-xs text-secondary-400">Kontakt anzeigen</p>
</div> </div>
<svg <ChevronRight className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
className="w-4 h-4 text-secondary-400 flex-shrink-0"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</a> </a>
); );
}; };
@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import type { MessageBlock } from '@/store/commStore'; import type { MessageBlock } from '@/store/commStore';
import { Download, FileText } from 'lucide-react';
interface FileBlockProps { interface FileBlockProps {
block: MessageBlock; block: MessageBlock;
@@ -14,37 +15,11 @@ function formatFileSize(bytes: number | undefined): string {
} }
const fileIcon = ( const fileIcon = (
<svg <FileText className="w-8 h-8 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
className="w-8 h-8 text-secondary-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z M14 2v6h6 M8 13h8 M8 17h5"
/>
</svg>
); );
const downloadIcon = ( const downloadIcon = (
<svg <Download className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
/>
</svg>
); );
const FileBlock: React.FC<FileBlockProps> = ({ block }) => { const FileBlock: React.FC<FileBlockProps> = ({ block }) => {
@@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import type { MessageBlock } from '@/store/commStore'; import type { MessageBlock } from '@/store/commStore';
import { AppWindow } from 'lucide-react';
interface MiniAppBlockProps { interface MiniAppBlockProps {
block: MessageBlock; block: MessageBlock;
} }
@@ -14,20 +16,7 @@ const MiniAppBlock: React.FC<MiniAppBlockProps> = ({ block }) => {
return ( return (
<div className="border-2 border-dashed border-secondary-300 rounded-lg p-4 text-center bg-secondary-50"> <div className="border-2 border-dashed border-secondary-300 rounded-lg p-4 text-center bg-secondary-50">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<svg <AppWindow className="w-8 h-8 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
className="w-8 h-8 text-secondary-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 6a2 2 0 012-2h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6z M9 9h6v6H9z"
/>
</svg>
<p className="text-sm font-medium text-secondary-600"> <p className="text-sm font-medium text-secondary-600">
Mini-App: {appId} Mini-App: {appId}
</p> </p>
@@ -6,6 +6,8 @@ import { EmptyState } from '@/components/ui/EmptyState';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { Loader2 } from 'lucide-react';
import { import {
type UnifiedContact, type UnifiedContact,
type ContactPerson, type ContactPerson,
@@ -145,10 +147,7 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12" data-testid="contact-detail-loading"> <div className="flex items-center justify-center py-12" data-testid="contact-detail-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
@@ -9,6 +9,7 @@ import {
useMoveContactToFolder, useMoveContactToFolder,
} from '@/api/hooks'; } from '@/api/hooks';
import { buildFolderTree, type ContactFolderTreeNode } from '@/api/contactFolders'; import { buildFolderTree, type ContactFolderTreeNode } from '@/api/contactFolders';
import { ChevronRight, Folder, Pencil, Plus, Tag, Trash2, Users } from 'lucide-react';
export type ContactFilter = 'all' | 'company' | 'person' | `tag:${string}` | `folder:${string}`; export type ContactFilter = 'all' | 'company' | 'person' | `tag:${string}` | `folder:${string}`;
@@ -22,28 +23,21 @@ export interface ContactFolderTreeProps {
// Icons // Icons
const icon = (path: string, cls = 'w-4 h-4') => ( const icon = (IconComp: React.ElementType, cls = 'w-4 h-4') => (
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <IconComp className={cls} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={path} />
</svg>
); );
const chevron = (open: boolean) => ( const chevron = (open: boolean) => (
<svg <ChevronRight className={clsx('w-3.5 h-3.5 transition-transform flex-shrink-0', open && 'rotate-90')} aria-hidden="true" strokeWidth={2} />
className={clsx('w-3.5 h-3.5 transition-transform flex-shrink-0', open && 'rotate-90')}
fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
); );
const ICONS = { const ICONS = {
all: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6-3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z', all: Users,
folder: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z', folder: Folder,
tag: 'M7 7h.01M7 3h5a1.99 1.99 0 01.832.184l4 2A2 2 0 0118 7v10a2 2 0 01-2 2H7a2 2 0 01-2-2V5a2 2 0 012-2z', tag: Tag,
edit: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z', edit: Pencil,
trash: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16', trash: Trash2,
plus: 'M12 4v16m8-8H4', plus: Plus,
}; };
// Context Menu // Context Menu
@@ -6,6 +6,8 @@ import { Pagination } from '@/components/ui/Pagination';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import type { UnifiedContact } from '@/api/hooks'; import type { UnifiedContact } from '@/api/hooks';
import { Loader2 } from 'lucide-react';
export type ContactViewMode = 'list' | 'table' | 'cards'; export type ContactViewMode = 'list' | 'table' | 'cards';
export interface ContactListProps { export interface ContactListProps {
@@ -77,10 +79,7 @@ export function ContactList({
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12" data-testid="contact-list-loading"> <div className="flex items-center justify-center py-12" data-testid="contact-list-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
+8 -22
View File
@@ -8,6 +8,7 @@ import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { DmsFile } from '@/api/dms'; import type { DmsFile } from '@/api/dms';
import { getFileIcon, formatFileSize } from './FileExplorer'; import { getFileIcon, formatFileSize } from './FileExplorer';
import { Download, Eye, FileText, Share2, Trash2, X } from 'lucide-react';
function formatDate(dateStr: string | null | undefined): string { function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return '-'; if (!dateStr) return '-';
@@ -37,9 +38,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
if (!file) { if (!file) {
return ( return (
<div className="flex flex-col items-center justify-center h-full p-6 text-center" data-testid="file-details-empty"> <div className="flex flex-col items-center justify-center h-full p-6 text-center" data-testid="file-details-empty">
<svg className="w-12 h-12 text-secondary-300 mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <FileText className="w-12 h-12 text-secondary-300 mb-3" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<p className="text-sm font-medium text-secondary-600">{t('dms.noFileSelected')}</p> <p className="text-sm font-medium text-secondary-600">{t('dms.noFileSelected')}</p>
<p className="mt-1 text-xs text-secondary-400">{t('dms.noFileSelectedHint')}</p> <p className="mt-1 text-xs text-secondary-400">{t('dms.noFileSelectedHint')}</p>
</div> </div>
@@ -60,9 +59,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
className="p-1 rounded text-secondary-400 hover:text-secondary-600 hover:bg-secondary-100 min-h-touch min-w-touch" className="p-1 rounded text-secondary-400 hover:text-secondary-600 hover:bg-secondary-100 min-h-touch min-w-touch"
aria-label={t('common.close')} aria-label={t('common.close')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
@@ -82,9 +79,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
/> />
</div> </div>
) : ( ) : (
<svg className={clsx('w-24 h-24', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <icon.icon className={clsx('w-24 h-24', icon.color)} aria-hidden="true" strokeWidth={1} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d={icon.path} />
</svg>
)} )}
</div> </div>
@@ -97,19 +92,14 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
onClick={() => onPreview(file)} onClick={() => onPreview(file)}
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-primary-50 text-primary-700 hover:bg-primary-100 min-h-touch" className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-primary-50 text-primary-700 hover:bg-primary-100 min-h-touch"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
{t('dms.preview')} {t('dms.preview')}
</button> </button>
<button <button
onClick={() => onShare(file)} onClick={() => onShare(file)}
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-secondary-50 text-secondary-700 hover:bg-secondary-100 min-h-touch" className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-secondary-50 text-secondary-700 hover:bg-secondary-100 min-h-touch"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4M18 12a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{t('dms.share')} {t('dms.share')}
</button> </button>
<a <a
@@ -117,18 +107,14 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
download={file.name} download={file.name}
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-secondary-50 text-secondary-700 hover:bg-secondary-100 min-h-touch" className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-secondary-50 text-secondary-700 hover:bg-secondary-100 min-h-touch"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Download className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
{t('dms.download')} {t('dms.download')}
</a> </a>
<button <button
onClick={() => onDelete(file)} onClick={() => onDelete(file)}
className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-danger-50 text-danger-700 hover:bg-danger-100 min-h-touch" className="inline-flex items-center justify-center gap-1.5 px-3 py-2 rounded-md text-xs font-medium bg-danger-50 text-danger-700 hover:bg-danger-100 min-h-touch"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
{t('dms.delete')} {t('dms.delete')}
</button> </button>
</div> </div>
+18 -37
View File
@@ -7,27 +7,28 @@ import React, { useMemo } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { DmsFile } from '@/api/dms'; import type { DmsFile } from '@/api/dms';
import { Archive, ChevronDown, ChevronUp, Eye, FileText, Image, Loader2, Share2, Trash2 } from 'lucide-react';
export function getFileIcon(mimeType: string): { path: string; color: string } { export function getFileIcon(mimeType: string): { icon: React.ElementType; color: string } {
if (mimeType === 'application/pdf') { if (mimeType === 'application/pdf') {
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-danger-500' }; return { icon: FileText, color: 'text-danger-500' };
} }
if (mimeType.startsWith('image/')) { if (mimeType.startsWith('image/')) {
return { path: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-accent-500' }; return { icon: Image, color: 'text-accent-500' };
} }
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) { if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) {
return { path: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-success-500' }; return { icon: FileText, color: 'text-success-500' };
} }
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) { if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) {
return { path: 'M7 8h10M7 16h10M7 12h6m-7 8h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-warning-500' }; return { icon: FileText, color: 'text-warning-500' };
} }
if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) { if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) {
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-primary-500' }; return { icon: FileText, color: 'text-primary-500' };
} }
if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) { if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) {
return { path: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4', color: 'text-secondary-500' }; return { icon: Archive, color: 'text-secondary-500' };
} }
return { path: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', color: 'text-secondary-400' }; return { icon: FileText, color: 'text-secondary-400' };
} }
export function formatFileSize(bytes: number): string { export function formatFileSize(bytes: number): string {
@@ -101,9 +102,7 @@ function SortHeader({
> >
{label} {label}
{isActive && ( {isActive && (
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> sortOrder === 'asc' ? <ChevronUp className="w-3 h-3" aria-hidden="true" strokeWidth={2} /> : <ChevronDown className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'} />
</svg>
)} )}
</button> </button>
); );
@@ -130,10 +129,7 @@ function ActionButtons({
aria-label={t('dms.preview')} aria-label={t('dms.preview')}
title={t('dms.preview')} title={t('dms.preview')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onFileShare(file); }} onClick={(e) => { e.stopPropagation(); onFileShare(file); }}
@@ -141,9 +137,7 @@ function ActionButtons({
aria-label={t('dms.share')} aria-label={t('dms.share')}
title={t('dms.share')} title={t('dms.share')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4M18 12a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onFileDelete(file); }} onClick={(e) => { e.stopPropagation(); onFileDelete(file); }}
@@ -151,9 +145,7 @@ function ActionButtons({
aria-label={t('dms.delete')} aria-label={t('dms.delete')}
title={t('dms.delete')} title={t('dms.delete')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button> </button>
</div> </div>
); );
@@ -198,10 +190,7 @@ export function FileExplorer({
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12" data-testid="file-explorer-loading"> <div className="flex items-center justify-center py-12" data-testid="file-explorer-loading">
<svg className="animate-spin h-6 w-6 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-6 w-6 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-secondary-500 text-sm">{t('dms.loading')}</span> <span className="ml-2 text-secondary-500 text-sm">{t('dms.loading')}</span>
</div> </div>
); );
@@ -210,9 +199,7 @@ export function FileExplorer({
if (files.length === 0) { if (files.length === 0) {
return ( return (
<div className="text-center py-12" data-testid="file-explorer-empty"> <div className="text-center py-12" data-testid="file-explorer-empty">
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p> <p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p>
</div> </div>
); );
@@ -254,9 +241,7 @@ export function FileExplorer({
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500 flex-shrink-0" className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500 flex-shrink-0"
aria-label={t('dms.bulkSelect')} aria-label={t('dms.bulkSelect')}
/> />
<svg className={clsx('w-5 h-5 flex-shrink-0', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <icon.icon className={clsx('w-5 h-5 flex-shrink-0', icon.color)} aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
</svg>
<span className="text-sm font-medium text-secondary-900 truncate flex-1" title={file.name}>{file.name}</span> <span className="text-sm font-medium text-secondary-900 truncate flex-1" title={file.name}>{file.name}</span>
<span className="text-xs text-secondary-500 flex-shrink-0">{formatFileSize(getFileSize(file))}</span> <span className="text-xs text-secondary-500 flex-shrink-0">{formatFileSize(getFileSize(file))}</span>
<span className="text-xs text-secondary-400 flex-shrink-0 hidden sm:inline">{formatDate(file.updated_at || file.created_at)}</span> <span className="text-xs text-secondary-400 flex-shrink-0 hidden sm:inline">{formatDate(file.updated_at || file.created_at)}</span>
@@ -327,9 +312,7 @@ export function FileExplorer({
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<svg className={clsx('w-5 h-5 flex-shrink-0', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <icon.icon className={clsx('w-5 h-5 flex-shrink-0', icon.color)} aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
</svg>
<span className="font-medium text-secondary-900 truncate" title={file.name}>{file.name}</span> <span className="font-medium text-secondary-900 truncate" title={file.name}>{file.name}</span>
</div> </div>
</td> </td>
@@ -415,9 +398,7 @@ export function FileExplorer({
/> />
</div> </div>
) : ( ) : (
<svg className={clsx(iconSize, icon.color, isSmall ? 'mb-0.5' : 'mb-1')} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <icon.icon className={clsx(iconSize, icon.color, isSmall ? 'mb-0.5' : 'mb-1')} aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
</svg>
)} )}
<p className={clsx('font-medium text-secondary-900 truncate w-full', isSmall ? 'text-[10px]' : 'text-xs')} title={file.name}>{file.name}</p> <p className={clsx('font-medium text-secondary-900 truncate w-full', isSmall ? 'text-[10px]' : 'text-xs')} title={file.name}>{file.name}</p>
{!isSmall && ( {!isSmall && (
+7 -44
View File
@@ -7,35 +7,9 @@ import React, { useMemo } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { DmsFile } from '@/api/dms'; import type { DmsFile } from '@/api/dms';
import { Eye, FileText, Share2, Trash2 } from 'lucide-react';
function getFileIcon(mimeType: string): { path: string; color: string } { import { getFileIcon, formatFileSize } from "./FileExplorer";
if (mimeType === 'application/pdf') {
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-danger-500' };
}
if (mimeType.startsWith('image/')) {
return { path: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-accent-500' };
}
if (mimeType.includes('spreadsheet') || mimeType.includes('excel')) {
return { path: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-success-500' };
}
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) {
return { path: 'M7 8h10M7 16h10M7 12h6m-7 8h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z', color: 'text-warning-500' };
}
if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('word')) {
return { path: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', color: 'text-primary-500' };
}
if (mimeType.includes('zip') || mimeType.includes('compressed') || mimeType.includes('archive')) {
return { path: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4', color: 'text-secondary-500' };
}
return { path: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', color: 'text-secondary-400' };
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
export interface FileGridProps { export interface FileGridProps {
files: DmsFile[]; files: DmsFile[];
@@ -81,9 +55,7 @@ export function FileGrid({
if (files.length === 0) { if (files.length === 0) {
return ( return (
<div className="text-center py-12" data-testid="file-grid-empty"> <div className="text-center py-12" data-testid="file-grid-empty">
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p> <p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p>
</div> </div>
); );
@@ -114,9 +86,7 @@ export function FileGrid({
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500" className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
aria-label={t('dms.bulkSelect')} aria-label={t('dms.bulkSelect')}
/> />
<svg className={clsx('w-10 h-10', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <icon.icon className={clsx('w-10 h-10', icon.color)} aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
</svg>
</div> </div>
</div> </div>
<p className="text-sm font-medium text-secondary-900 truncate" title={file.name}>{file.name}</p> <p className="text-sm font-medium text-secondary-900 truncate" title={file.name}>{file.name}</p>
@@ -131,10 +101,7 @@ export function FileGrid({
aria-label={t('dms.preview')} aria-label={t('dms.preview')}
title={t('dms.preview')} title={t('dms.preview')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onFileShare(file); }} onClick={(e) => { e.stopPropagation(); onFileShare(file); }}
@@ -142,9 +109,7 @@ export function FileGrid({
aria-label={t('dms.share')} aria-label={t('dms.share')}
title={t('dms.share')} title={t('dms.share')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.7 10.7l6.6-3.4M8.7 13.3l6.6 3.4M18 12a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onFileDelete(file); }} onClick={(e) => { e.stopPropagation(); onFileDelete(file); }}
@@ -152,9 +117,7 @@ export function FileGrid({
aria-label={t('dms.delete')} aria-label={t('dms.delete')}
title={t('dms.delete')} title={t('dms.delete')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button> </button>
</div> </div>
</div> </div>
@@ -10,6 +10,7 @@ import { Modal } from '@/components/ui/Modal';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { getFilePreviewUrl, type DmsFile } from '@/api/dms'; import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
import { FileText } from 'lucide-react';
export interface FilePreviewModalProps { export interface FilePreviewModalProps {
open: boolean; open: boolean;
@@ -81,9 +82,7 @@ export function FilePreviewModal({ open, file, onClose }: FilePreviewModalProps)
{!isPdf && !isImage && ( {!isPdf && !isImage && (
<div className="text-center py-12" data-testid="no-preview-available"> <div className="text-center py-12" data-testid="no-preview-available">
<svg className="mx-auto h-12 w-12 text-secondary-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<p className="mt-2 text-sm text-secondary-500">{t('dms.preview')}</p> <p className="mt-2 text-sm text-secondary-500">{t('dms.preview')}</p>
</div> </div>
)} )}
+4 -9
View File
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { DmsFolder } from '@/api/dms'; import type { DmsFolder } from '@/api/dms';
import { ChevronDown, Folder, Menu } from 'lucide-react';
interface FolderTreeItemProps { interface FolderTreeItemProps {
folder: DmsFolder; folder: DmsFolder;
@@ -52,15 +53,11 @@ function FolderTreeItem({ folder, level, selectedFolderId, onSelect }: FolderTre
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600" className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
aria-label={expanded ? t('dms.folders') : t('dms.folders')} aria-label={expanded ? t('dms.folders') : t('dms.folders')}
> >
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
</svg>
</button> </button>
)} )}
{!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />} {!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />}
<svg className="w-4 h-4 text-warning-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Folder className="w-4 h-4 text-warning-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<span className="truncate">{folder.name}</span> <span className="truncate">{folder.name}</span>
{typeof folder.file_count === 'number' && folder.file_count > 0 && ( {typeof folder.file_count === 'number' && folder.file_count > 0 && (
<span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span> <span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span>
@@ -125,9 +122,7 @@ export function FolderTree({ folders, selectedFolderId, onSelect, loading = fals
role="button" role="button"
aria-label={t('dms.allFiles')} aria-label={t('dms.allFiles')}
> >
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Menu className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span>{t('dms.allFiles')}</span> <span>{t('dms.allFiles')}</span>
</div> </div>
</li> </li>
+9 -24
View File
@@ -12,6 +12,7 @@ import React, { useState } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { DmsFolder, DmsFile } from '@/api/dms'; import type { DmsFolder, DmsFile } from '@/api/dms';
import { ChevronDown, FileText, Folder, Home, Menu, Users } from 'lucide-react';
interface SourceTreeFolderItemProps { interface SourceTreeFolderItemProps {
folder: DmsFolder; folder: DmsFolder;
@@ -109,15 +110,11 @@ function SourceTreeFolderItem({
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600" className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
aria-label={expanded ? t('dms.folders') : t('dms.folders')} aria-label={expanded ? t('dms.folders') : t('dms.folders')}
> >
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
</svg>
</button> </button>
)} )}
{!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />} {!hasChildren && <span className="w-4 flex-shrink-0" aria-hidden="true" />}
<svg className={clsx('w-4 h-4 flex-shrink-0', isDropTarget ? 'text-primary-500' : 'text-warning-500')} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Folder className={clsx('w-4 h-4 flex-shrink-0', isDropTarget ? 'text-primary-500' : 'text-warning-500')} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
<span className="truncate">{folder.name}</span> <span className="truncate">{folder.name}</span>
{typeof folder.file_count === 'number' && folder.file_count > 0 && ( {typeof folder.file_count === 'number' && folder.file_count > 0 && (
<span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span> <span className="ml-auto text-xs text-secondary-400">{folder.file_count}</span>
@@ -187,9 +184,7 @@ function SourceSection({ title, icon, selected, onClick, children, defaultExpand
className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600" className="flex-shrink-0 w-4 h-4 flex items-center justify-center text-secondary-400 hover:text-secondary-600"
aria-label={expanded ? 'Collapse' : 'Expand'} aria-label={expanded ? 'Collapse' : 'Expand'}
> >
<svg className="w-3 h-3 transition-transform" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
</svg>
</button> </button>
{icon} {icon}
<span className="truncate">{title}</span> <span className="truncate">{title}</span>
@@ -298,9 +293,7 @@ export function SourceTree({
role="button" role="button"
aria-label={t('dms.allFiles')} aria-label={t('dms.allFiles')}
> >
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Menu className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
<span>{t('dms.allFiles')}</span> <span>{t('dms.allFiles')}</span>
</div> </div>
@@ -315,9 +308,7 @@ export function SourceTree({
} }
}} }}
icon={ icon={
<svg className="w-4 h-4 text-primary-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Folder className="w-4 h-4 text-primary-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
} }
> >
{folders.length === 0 ? ( {folders.length === 0 ? (
@@ -346,9 +337,7 @@ export function SourceTree({
onSelectFolder(null); onSelectFolder(null);
}} }}
icon={ icon={
<svg className="w-4 h-4 text-accent-500 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Users className="w-4 h-4 text-accent-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
} }
> >
{sharedFiles.length === 0 ? ( {sharedFiles.length === 0 ? (
@@ -367,9 +356,7 @@ export function SourceTree({
role="button" role="button"
aria-label={file.name} aria-label={file.name}
> >
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <FileText className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
<span className="truncate">{file.name}</span> <span className="truncate">{file.name}</span>
</div> </div>
</li> </li>
@@ -386,9 +373,7 @@ export function SourceTree({
onSelectFolder(null); onSelectFolder(null);
}} }}
icon={ icon={
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Home className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12H3l9-9 9 9h-2M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7M9 21v-6h6v6" />
</svg>
} }
defaultExpanded={false} defaultExpanded={false}
> >
@@ -8,6 +8,7 @@ import clsx from 'clsx';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { uploadFile } from '@/api/dms'; import { uploadFile } from '@/api/dms';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { Upload } from 'lucide-react';
export interface UploadDropzoneProps { export interface UploadDropzoneProps {
folderId: string | null; folderId: string | null;
@@ -124,9 +125,7 @@ export function UploadDropzone({ folderId, onUploaded }: UploadDropzoneProps) {
className="hidden" className="hidden"
aria-hidden="true" aria-hidden="true"
/> />
<svg className="mx-auto h-10 w-10 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Upload className="mx-auto h-10 w-10 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p className="mt-2 text-sm text-secondary-600">{t('dms.uploadDropHere')}</p> <p className="mt-2 text-sm text-secondary-600">{t('dms.uploadDropHere')}</p>
</div> </div>
{uploads.length > 0 && ( {uploads.length > 0 && (
+10 -27
View File
@@ -7,41 +7,30 @@ import { useUIStore } from '@/store/uiStore';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useUsers, useGroups } from '@/api/hooks'; import { useUsers, useGroups } from '@/api/hooks';
import { Avatar } from '@/components/ui/Avatar'; import { Avatar } from '@/components/ui/Avatar';
import { Bell, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Users, X } from 'lucide-react';
const robotIcon = (className: string) => ( const robotIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Bot className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 2a4 4 0 014 4v1h1a3 3 0 013 3v6a3 3 0 01-3 3h-1v1a4 4 0 01-4 4H8a4 4 0 01-4-4v-1H3a3 3 0 01-3-3V10a3 3 0 013-3h1V6a4 4 0 014-4z M9 10h.01M15 10h.01M9 15h6" />
</svg>
); );
const bellIcon = (className: string) => ( const bellIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Bell className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
); );
const bulbIcon = (className: string) => ( const bulbIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Lightbulb className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
); );
const teamIcon = (className: string) => ( const teamIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Users className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6-3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
); );
const chatBubbleIcon = (className: string) => ( const chatBubbleIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <MessageSquare className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
); );
const chevronRightIcon = ( const chevronRightIcon = (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
); );
interface TabDef { interface TabDef {
@@ -94,9 +83,7 @@ function TeamPanel() {
{groups.map((g) => ( {groups.map((g) => (
<div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors"> <div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
<div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0">
<svg className="w-4 h-4 text-secondary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p> <p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p>
@@ -201,9 +188,7 @@ export function AISidebar() {
aria-label="Benachrichtigung löschen" aria-label="Benachrichtigung löschen"
onClick={() => removeNotification(i)} onClick={() => removeNotification(i)}
> >
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <X className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
))} ))}
@@ -283,9 +268,7 @@ export function AISidebar() {
aria-label="Zurück" aria-label="Zurück"
data-testid="ai-sidebar-back" data-testid="ai-sidebar-back"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-5 h-5" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">Zurück</span> <span className="text-sm font-medium">Zurück</span>
</button> </button>
</div> </div>
@@ -15,55 +15,40 @@ import {
import { useCommWebSocket } from '@/hooks/useCommWebSocket'; import { useCommWebSocket } from '@/hooks/useCommWebSocket';
import type { Conversation, Message } from '@/store/commStore'; import type { Conversation, Message } from '@/store/commStore';
import { BlockRenderer } from '@/components/comm/blocks'; import { BlockRenderer } from '@/components/comm/blocks';
import { Bell, Bookmark, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Send, Users } from 'lucide-react';
// ─── Icons (identical to AISidebar) ─── // ─── Icons (identical to AISidebar) ───
const robotIcon = (className: string) => ( const robotIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Bot className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 2a4 4 0 014 4v1h1a3 3 0 013 3v6a3 3 0 01-3 3h-1v1a4 4 0 01-4 4H8a4 4 0 01-4-4v-1H3a3 3 0 01-3-3V10a3 3 0 013-3h1V6a4 4 0 014-4z M9 10h.01M15 10h.01M9 15h6" />
</svg>
); );
const bellIcon = (className: string) => ( const bellIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Bell className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
); );
const bulbIcon = (className: string) => ( const bulbIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Lightbulb className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
); );
const teamIcon = (className: string) => ( const teamIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Users className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6-3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
); );
const chatBubbleIcon = (className: string) => ( const chatBubbleIcon = (className: string) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <MessageSquare className={className} aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
); );
const chevronRightIcon = ( const chevronRightIcon = (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
); );
const sendIcon = ( const sendIcon = (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Send className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
); );
const pinIcon = ( const pinIcon = (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Bookmark className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
</svg>
); );
// ─── Types ─── // ─── Types ───
@@ -128,9 +113,7 @@ function TeamPanel({ onStartDirectChat }: { onStartDirectChat: (userId: string,
{groups.map((g) => ( {groups.map((g) => (
<div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors"> <div key={g.id} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-secondary-50 transition-colors">
<div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-secondary-200 flex items-center justify-center flex-shrink-0">
<svg className="w-4 h-4 text-secondary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p> <p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p>
@@ -660,9 +643,7 @@ export function MessageSidebar() {
aria-label="Zurück" aria-label="Zurück"
data-testid="message-sidebar-back" data-testid="message-sidebar-back"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-5 h-5" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">Zurück</span> <span className="text-sm font-medium">Zurück</span>
</button> </button>
</div> </div>
+8 -21
View File
@@ -3,6 +3,7 @@ import clsx from 'clsx';
import { NavLink, useLocation } from 'react-router-dom'; import { NavLink, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useUIStore } from '@/store/uiStore'; import { useUIStore } from '@/store/uiStore';
import { Calendar, ChevronRight, FileText, Home, Mail, Monitor, Users } from 'lucide-react';
interface NavLeaf { interface NavLeaf {
to: string; to: string;
@@ -21,33 +22,19 @@ interface NavSingleItem {
icon: React.ReactNode; icon: React.ReactNode;
} }
const navIcon = (path: string) => (
<svg className="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={path} />
</svg>
);
const chevronIcon = (expanded: boolean) => ( const chevronIcon = (expanded: boolean) => (
<svg <ChevronRight className={clsx('w-4 h-4 flex-shrink-0 transition-transform duration-200', expanded ? 'rotate-90' : '')} aria-hidden="true" strokeWidth={2} />
className={clsx('w-4 h-4 flex-shrink-0 transition-transform duration-200', expanded ? 'rotate-90' : '')}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
); );
const singleItems: NavSingleItem[] = [ const singleItems: NavSingleItem[] = [
{ to: '/dashboard', labelKey: 'nav.dashboard', icon: navIcon('M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6') }, { to: '/dashboard', labelKey: 'nav.dashboard', icon: <Home className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} /> },
{ to: '/contacts', labelKey: 'nav.contacts', icon: navIcon('M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6-3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z') }, { to: '/contacts', labelKey: 'nav.contacts', icon: <Users className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} /> },
]; ];
const treeItems: NavTreeItem[] = [ const treeItems: NavTreeItem[] = [
{ {
labelKey: 'nav.calendar', labelKey: 'nav.calendar',
icon: navIcon('M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'), icon: <Calendar className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />,
children: [ children: [
{ to: '/calendar', labelKey: 'nav.calendar' }, { to: '/calendar', labelKey: 'nav.calendar' },
{ to: '/calendar/kanban', labelKey: 'nav.calendarKanban' }, { to: '/calendar/kanban', labelKey: 'nav.calendarKanban' },
@@ -55,7 +42,7 @@ const treeItems: NavTreeItem[] = [
}, },
{ {
labelKey: 'nav.files', labelKey: 'nav.files',
icon: navIcon('M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z'), icon: <FileText className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />,
children: [ children: [
{ to: '/dms', labelKey: 'nav.files' }, { to: '/dms', labelKey: 'nav.files' },
{ to: '/dms/trash', labelKey: 'nav.filesTrash' }, { to: '/dms/trash', labelKey: 'nav.filesTrash' },
@@ -63,7 +50,7 @@ const treeItems: NavTreeItem[] = [
}, },
{ {
labelKey: 'nav.email', labelKey: 'nav.email',
icon: navIcon('M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z'), icon: <Mail className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} />,
children: [ children: [
{ to: '/mail', labelKey: 'nav.email' }, { to: '/mail', labelKey: 'nav.email' },
{ to: '/mail/settings', labelKey: 'nav.emailSettings' }, { to: '/mail/settings', labelKey: 'nav.emailSettings' },
@@ -72,7 +59,7 @@ const treeItems: NavTreeItem[] = [
]; ];
const bottomItems: NavSingleItem[] = [ const bottomItems: NavSingleItem[] = [
{ to: '/ai-assistant', labelKey: 'nav.aiAssistant', icon: navIcon('M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z') }, { to: '/ai-assistant', labelKey: 'nav.aiAssistant', icon: <Monitor className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} /> },
]; ];
export function Sidebar() { export function Sidebar() {
+5 -12
View File
@@ -8,6 +8,7 @@ import { useLogout } from '@/api/hooks';
import { Avatar } from '@/components/ui/Avatar'; import { Avatar } from '@/components/ui/Avatar';
import { SearchDropdown } from '@/components/shared/SearchDropdown'; import { SearchDropdown } from '@/components/shared/SearchDropdown';
import { SuggestionBadge } from '@/components/ai/SuggestionBadge'; import { SuggestionBadge } from '@/components/ai/SuggestionBadge';
import { Building, ChevronDown, Menu } from 'lucide-react';
export function TopBar() { export function TopBar() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -52,9 +53,7 @@ export function TopBar() {
aria-label="Seitenleiste ein-/ausklappen" aria-label="Seitenleiste ein-/ausklappen"
aria-expanded={true} aria-expanded={true}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Menu className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button> </button>
{/* Tenant switcher */} {/* Tenant switcher */}
@@ -64,13 +63,9 @@ export function TopBar() {
className="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-secondary-100 min-h-touch text-sm font-medium text-secondary-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500" className="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-secondary-100 min-h-touch text-sm font-medium text-secondary-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
aria-label={t('topbar.switchTenant')} aria-label={t('topbar.switchTenant')}
> >
<svg className="w-4 h-4 text-secondary-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Building className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
{currentTenant?.name || tenants[0]?.name || ''} {currentTenant?.name || tenants[0]?.name || ''}
<svg className="w-3 h-3 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronDown className="w-3 h-3 text-secondary-400" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button> </button>
</div> </div>
)} )}
@@ -95,9 +90,7 @@ export function TopBar() {
<span className="hidden md:block text-sm font-medium text-secondary-700 max-w-24 truncate"> <span className="hidden md:block text-sm font-medium text-secondary-700 max-w-24 truncate">
{user?.first_name || ''} {user?.first_name || ''}
</span> </span>
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronDown className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button> </button>
{userMenuOpen && ( {userMenuOpen && (
<div <div
@@ -14,6 +14,7 @@ import { RichTextEditor } from './RichTextEditor';
import type { Mail, MailSignature, SendMailPayload, ReplyPayload, ForwardPayload, MailDraftPayload } from '@/api/mail'; import type { Mail, MailSignature, SendMailPayload, ReplyPayload, ForwardPayload, MailDraftPayload } from '@/api/mail';
import { uploadAttachment, type UploadedAttachment, replaceSignatureVariables } from '@/api/mail'; import { uploadAttachment, type UploadedAttachment, replaceSignatureVariables } from '@/api/mail';
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { FileText, Paperclip, X } from 'lucide-react';
export type ComposeMode = 'new' | 'reply' | 'forward' | 'draft'; export type ComposeMode = 'new' | 'reply' | 'forward' | 'draft';
@@ -309,9 +310,7 @@ export function ComposeModal({
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
isLoading={uploadingFile} isLoading={uploadingFile}
icon={ icon={
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Paperclip className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
</svg>
} }
> >
{t('mail.addAttachment')} {t('mail.addAttachment')}
@@ -322,9 +321,7 @@ export function ComposeModal({
<ul className="mt-2 space-y-1"> <ul className="mt-2 space-y-1">
{attachments.map((att) => ( {attachments.map((att) => (
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md bg-secondary-50"> <li key={att.id} className="flex items-center gap-3 p-2 rounded-md bg-secondary-50">
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <FileText className="w-5 h-5 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-secondary-800 truncate">{att.filename}</p> <p className="text-sm text-secondary-800 truncate">{att.filename}</p>
<p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p> <p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p>
@@ -335,9 +332,7 @@ export function ComposeModal({
className="p-1 rounded hover:bg-secondary-200 text-secondary-500" className="p-1 rounded hover:bg-secondary-200 text-secondary-500"
aria-label={t('common.remove')} aria-label={t('common.remove')}
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</li> </li>
))} ))}
@@ -10,6 +10,7 @@ import { Card } from '@/components/ui/Card';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { Loader2, X } from 'lucide-react';
import { import {
fetchLabels, fetchLabels,
createLabel, createLabel,
@@ -81,10 +82,7 @@ export function LabelManager() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-8" data-testid="label-manager-loading"> <div className="flex items-center justify-center py-8" data-testid="label-manager-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div> </div>
); );
} }
@@ -155,9 +153,7 @@ export function LabelManager() {
data-testid={`delete-label-${label.id}`} data-testid={`delete-label-${label.id}`}
type="button" type="button"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
))} ))}
+3 -7
View File
@@ -10,6 +10,7 @@ import type { Mail, MailAttachment } from '@/api/mail';
import { decodeMimeHeader } from '@/api/mail'; import { decodeMimeHeader } from '@/api/mail';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { FileText, Loader2 } from 'lucide-react';
export interface MailDetailProps { export interface MailDetailProps {
mail: Mail | null; loading: boolean; mail: Mail | null; loading: boolean;
@@ -75,10 +76,7 @@ export function MailDetail({
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12" data-testid="mail-detail-loading"> <div className="flex items-center justify-center py-12" data-testid="mail-detail-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
@@ -158,9 +156,7 @@ export function MailDetail({
<ul className="space-y-1 md: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"> <FileText className="w-5 h-5 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-secondary-800 truncate">{decodeMimeHeader(att.filename)}</p> <p className="text-sm text-secondary-800 truncate">{decodeMimeHeader(att.filename)}</p>
<p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p> <p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p>
@@ -14,6 +14,7 @@ import type { MailAccount, MailFolder } from '@/api/mail';
import { emptyFolder } from '@/api/mail'; import { emptyFolder } from '@/api/mail';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { ChevronRight, Folder, Loader2 } from 'lucide-react';
/** /**
* Map IMAP folder names to German display names. * Map IMAP folder names to German display names.
@@ -88,23 +89,13 @@ function buildFolderTree(folders: MailFolder[], accountId: string): MailFolder[]
function ChevronIcon({ expanded }: { expanded: boolean }) { function ChevronIcon({ expanded }: { expanded: boolean }) {
return ( return (
<svg <ChevronRight className={clsx('w-4 h-4 transition-transform flex-shrink-0', expanded && 'rotate-90')} aria-hidden="true" strokeWidth={2} />
className={clsx('w-4 h-4 transition-transform flex-shrink-0', expanded && 'rotate-90')}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
); );
} }
function FolderIcon() { function FolderIcon() {
return ( return (
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Folder className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
); );
} }
@@ -304,10 +295,7 @@ export function MailFolderTree({ accounts, folders, selectedFolderId, onSelect,
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-8" data-testid="folder-tree-loading"> <div className="flex items-center justify-center py-8" data-testid="folder-tree-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
+5 -15
View File
@@ -10,6 +10,7 @@ import type { Mail } from '@/api/mail';
import { decodeMimeHeader } from '@/api/mail'; import { decodeMimeHeader } from '@/api/mail';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { Pagination } from '@/components/ui/Pagination'; import { Pagination } from '@/components/ui/Pagination';
import { ChevronUp, Loader2, Paperclip, Star } from 'lucide-react';
export interface MailListProps { export interface MailListProps {
mails: Mail[]; mails: Mail[];
@@ -70,10 +71,7 @@ export function MailList({
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-12" data-testid="mail-list-loading"> <div className="flex items-center justify-center py-12" data-testid="mail-list-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
@@ -132,11 +130,7 @@ export function MailList({
title={sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')} title={sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}
data-testid="mail-sort-order" data-testid="mail-sort-order"
> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronUp className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
{sortOrder === 'asc'
? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />}
</svg>
<span>{sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}</span> <span>{sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}</span>
</button> </button>
</div> </div>
@@ -180,16 +174,12 @@ export function MailList({
{mail.flag_type === 'important' && <span className="text-sm"></span>} {mail.flag_type === 'important' && <span className="text-sm"></span>}
{mail.flag_type === 'question' && <span className="text-sm"></span>} {mail.flag_type === 'question' && <span className="text-sm"></span>}
{(!mail.flag_type || mail.flag_type === '') && ( {(!mail.flag_type || mail.flag_type === '') && (
<svg className="w-4 h-4 text-warning-500" fill="currentColor" viewBox="0 0 24 24"> <Star className="w-4 h-4 text-warning-500" fill="currentColor" />
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
</svg>
)} )}
</span> </span>
)} )}
{mail.has_attachments && ( {mail.has_attachments && (
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Paperclip className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 13l-3.586 3.586a2 2 0 01-2.828 0L5 12.828a2 2 0 010-2.828l5.657-5.657a2 2 0 012.828 0L17 6.343M14.828 8.172a2 2 0 00-2.828 0l-3.586 3.586a2 2 0 000 2.828l1.414 1.414a2 2 0 002.828 0" />
</svg>
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
@@ -5,6 +5,7 @@
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Search } from 'lucide-react';
export interface MailSearchBarProps { export interface MailSearchBarProps {
onSearch: (query: string) => void; onSearch: (query: string) => void;
@@ -38,9 +39,7 @@ export function MailSearchBar({ onSearch }: MailSearchBarProps) {
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-md hover:bg-secondary-100 min-h-touch min-w-touch" className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-md hover:bg-secondary-100 min-h-touch min-w-touch"
aria-label={t('common.search')} aria-label={t('common.search')}
> >
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Search className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</button> </button>
</form> </form>
); );
+3 -4
View File
@@ -9,6 +9,8 @@ import { Input } from '@/components/ui/Input';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { Loader2 } from 'lucide-react';
import { import {
importPgpKey, importPgpKey,
fetchPgpKeys, fetchPgpKeys,
@@ -83,10 +85,7 @@ export function PgpSettings() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-8" data-testid="pgp-settings-loading"> <div className="flex items-center justify-center py-8" data-testid="pgp-settings-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div> </div>
); );
} }
+20 -19
View File
@@ -16,6 +16,7 @@ import { TextStyle } from '@tiptap/extension-text-style';
import Image from '@tiptap/extension-image'; import Image from '@tiptap/extension-image';
import Placeholder from '@tiptap/extension-placeholder'; import Placeholder from '@tiptap/extension-placeholder';
import clsx from 'clsx'; import clsx from 'clsx';
import { AlignCenter, AlignLeft, AlignRight, Bold, ChevronsLeft, Code, Heading, Image as ImageIcon, Italic, Link2, List, Menu, Quote, Redo2, Strikethrough, Underline as UnderlineIcon, Undo2 } from 'lucide-react';
export interface RichTextEditorProps { export interface RichTextEditorProps {
content: string; content: string;
@@ -121,10 +122,10 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
<div className="flex flex-wrap items-center gap-0.5 border-b border-secondary-200 bg-secondary-50 px-2 py-1" data-testid="rich-text-toolbar"> <div className="flex flex-wrap items-center gap-0.5 border-b border-secondary-200 bg-secondary-50 px-2 py-1" data-testid="rich-text-toolbar">
{/* Undo / Redo */} {/* Undo / Redo */}
<ToolbarButton onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} label={t('common.undo')}> <ToolbarButton onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} label={t('common.undo')}>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" /></svg> <Undo2 className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()} label={t('common.redo')}> <ToolbarButton onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()} label={t('common.redo')}>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6" /></svg> <Redo2 className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<div className="w-px h-6 bg-secondary-200 mx-1" /> <div className="w-px h-6 bg-secondary-200 mx-1" />
@@ -144,58 +145,58 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
{/* Inline formatting */} {/* Inline formatting */}
<ToolbarButton onClick={() => editor.chain().focus().toggleBold().run()} active={editor.isActive('bold')} label={t('mail.bold')}> <ToolbarButton onClick={() => editor.chain().focus().toggleBold().run()} active={editor.isActive('bold')} label={t('mail.bold')}>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6V4z M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6v-8z" /></svg> <Bold className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleItalic().run()} active={editor.isActive('italic')} label={t('mail.italic')}> <ToolbarButton onClick={() => editor.chain().focus().toggleItalic().run()} active={editor.isActive('italic')} label={t('mail.italic')}>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 4l-7 16M10 4L3 20M4 8h6M14 8h6" /></svg> <Italic className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleUnderline().run()} active={editor.isActive('underline')} label="Underline"> <ToolbarButton onClick={() => editor.chain().focus().toggleUnderline().run()} active={editor.isActive('underline')} label="Underline">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 3v8a6 6 0 006 6 6 6 0 006-6V3M4 21h16" /></svg> <UnderlineIcon className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleStrike().run()} active={editor.isActive('strike')} label="Strikethrough"> <ToolbarButton onClick={() => editor.chain().focus().toggleStrike().run()} active={editor.isActive('strike')} label="Strikethrough">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 12h16M9 5l6 14M15 5l-6 14" /></svg> <Strikethrough className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<div className="w-px h-6 bg-secondary-200 mx-1" /> <div className="w-px h-6 bg-secondary-200 mx-1" />
{/* Lists */} {/* Lists */}
<ToolbarButton onClick={() => editor.chain().focus().toggleBulletList().run()} active={editor.isActive('bulletList')} label="Bullet list"> <ToolbarButton onClick={() => editor.chain().focus().toggleBulletList().run()} active={editor.isActive('bulletList')} label="Bullet list">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16M8 6h.01M8 12h.01M8 18h.01" /></svg> <Menu className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleOrderedList().run()} active={editor.isActive('orderedList')} label="Numbered list"> <ToolbarButton onClick={() => editor.chain().focus().toggleOrderedList().run()} active={editor.isActive('orderedList')} label="Numbered list">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 6h12M7 12h12M7 18h12M3 6h.01M3 12h.01M3 18h.01" /></svg> <List className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleBlockquote().run()} active={editor.isActive('blockquote')} label="Quote"> <ToolbarButton onClick={() => editor.chain().focus().toggleBlockquote().run()} active={editor.isActive('blockquote')} label="Quote">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5h6v6H3zM3 13h6v6H3zM15 5h6v6h-6zM15 13h6v6h-6z" /></svg> <Quote className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleCode().run()} active={editor.isActive('code')} label="Inline code"> <ToolbarButton onClick={() => editor.chain().focus().toggleCode().run()} active={editor.isActive('code')} label="Inline code">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 7l-5 5 5 5M14 7l5 5-5 5" /></svg> <ChevronsLeft className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().toggleCodeBlock().run()} active={editor.isActive('codeBlock')} label="Code block"> <ToolbarButton onClick={() => editor.chain().focus().toggleCodeBlock().run()} active={editor.isActive('codeBlock')} label="Code block">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l-3 3 3 3M16 9l3 3-3 3M13 5l-2 14" /></svg> <Code className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<div className="w-px h-6 bg-secondary-200 mx-1" /> <div className="w-px h-6 bg-secondary-200 mx-1" />
{/* Alignment */} {/* Alignment */}
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('left').run()} active={editor.isActive({ textAlign: 'left' })} label="Align left"> <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('left').run()} active={editor.isActive({ textAlign: 'left' })} label="Align left">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h10M4 18h12" /></svg> <AlignLeft className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('center').run()} active={editor.isActive({ textAlign: 'center' })} label="Align center"> <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('center').run()} active={editor.isActive({ textAlign: 'center' })} label="Align center">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M7 12h10M5 18h14" /></svg> <AlignCenter className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('right').run()} active={editor.isActive({ textAlign: 'right' })} label="Align right"> <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('right').run()} active={editor.isActive({ textAlign: 'right' })} label="Align right">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M10 12h10M8 18h12" /></svg> <AlignRight className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('justify').run()} active={editor.isActive({ textAlign: 'justify' })} label="Justify"> <ToolbarButton onClick={() => editor.chain().focus().setTextAlign('justify').run()} active={editor.isActive({ textAlign: 'justify' })} label="Justify">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg> <Menu className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<div className="w-px h-6 bg-secondary-200 mx-1" /> <div className="w-px h-6 bg-secondary-200 mx-1" />
{/* Color */} {/* Color */}
<label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title="Text color"> <label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title="Text color">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 8l5 8 5-8M12 16v4" /></svg> <Heading className="w-4 h-4" strokeWidth={2} />
<input <input
type="color" type="color"
className="sr-only" className="sr-only"
@@ -208,17 +209,17 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
{/* Link & Image */} {/* Link & Image */}
<ToolbarButton onClick={setLink} active={editor.isActive('link')} label={t('mail.insertLink')}> <ToolbarButton onClick={setLink} active={editor.isActive('link')} label={t('mail.insertLink')}>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg> <Link2 className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<ToolbarButton onClick={addImage} label="Insert image"> <ToolbarButton onClick={addImage} label="Insert image">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg> <ImageIcon className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
<div className="w-px h-6 bg-secondary-200 mx-1" /> <div className="w-px h-6 bg-secondary-200 mx-1" />
{/* Clear formatting */} {/* Clear formatting */}
<ToolbarButton onClick={() => editor.chain().focus().unsetAllMarks().run()} label="Clear formatting"> <ToolbarButton onClick={() => editor.chain().focus().unsetAllMarks().run()} label="Clear formatting">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M9 7l3-1m0 0l3 9a5.002 5.002 0 006.001 0M15 6l-3 1" /></svg> <Quote className="w-4 h-4" strokeWidth={2} />
</ToolbarButton> </ToolbarButton>
</div> </div>
+3 -4
View File
@@ -11,6 +11,8 @@ import { Card } from '@/components/ui/Card';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { Loader2 } from 'lucide-react';
import { import {
fetchRules, fetchRules,
createRule, createRule,
@@ -132,10 +134,7 @@ export function RuleEditor({ accountId }: { accountId: string }) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-8" data-testid="rule-editor-loading"> <div className="flex items-center justify-center py-8" data-testid="rule-editor-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div> </div>
); );
} }
@@ -12,6 +12,8 @@ import { EmptyState } from '@/components/ui/EmptyState';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { RichTextEditor } from './RichTextEditor'; import { RichTextEditor } from './RichTextEditor';
import { Loader2 } from 'lucide-react';
import { import {
fetchSignatures, fetchSignatures,
createSignature, createSignature,
@@ -112,10 +114,7 @@ export function SignatureManager() {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-8" data-testid="signature-manager-loading"> <div className="flex items-center justify-center py-8" data-testid="signature-manager-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div> </div>
); );
} }
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
import { fetchTemplates, substituteTemplate, type MailTemplate } from '@/api/mail'; import { fetchTemplates, substituteTemplate, type MailTemplate } from '@/api/mail';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { Loader2 } from 'lucide-react';
export interface TemplatePickerProps { export interface TemplatePickerProps {
onSelect: (body: string, subject: string) => void; onSelect: (body: string, subject: string) => void;
} }
@@ -55,10 +57,7 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-4" data-testid="template-picker-loading"> <div className="flex items-center justify-center py-4" data-testid="template-picker-loading">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
+3 -4
View File
@@ -13,6 +13,8 @@ import clsx from 'clsx';
import { Pagination } from '@/components/ui/Pagination'; import { Pagination } from '@/components/ui/Pagination';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Loader2 } from 'lucide-react';
export interface DataGridProps<T> { export interface DataGridProps<T> {
columns: ColumnDef<T, any>[]; columns: ColumnDef<T, any>[];
data: T[]; data: T[];
@@ -107,10 +109,7 @@ export function DataGrid<T extends Record<string, any>>({
<tr> <tr>
<td colSpan={columns.length} className="px-6 py-8 text-center text-secondary-500"> <td colSpan={columns.length} className="px-6 py-8 text-center text-secondary-500">
<span className="inline-flex items-center gap-2"> <span className="inline-flex items-center gap-2">
<svg className="animate-spin motion-reduce:animate-none h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin motion-reduce:animate-none h-5 w-5" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
{t('common.loading')} {t('common.loading')}
</span> </span>
</td> </td>
@@ -3,6 +3,7 @@ import clsx from 'clsx';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useGlobalSearch, SearchResult } from '@/api/hooks'; import { useGlobalSearch, SearchResult } from '@/api/hooks';
import { Search } from 'lucide-react';
export interface SearchDropdownProps { export interface SearchDropdownProps {
placeholder?: string; placeholder?: string;
@@ -96,9 +97,7 @@ export function SearchDropdown({ placeholder }: SearchDropdownProps) {
aria-controls="search-results-list" aria-controls="search-results-list"
aria-autocomplete="list" aria-autocomplete="list"
/> />
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
{open && query.trim() && ( {open && query.trim() && (
<div <div
className="absolute top-full left-0 mt-1 w-96 bg-white rounded-md shadow-lg border border-secondary-200 z-50 max-h-96 overflow-y-auto" className="absolute top-full left-0 mt-1 w-96 bg-white rounded-md shadow-lg border border-secondary-200 z-50 max-h-96 overflow-y-auto"
+2 -3
View File
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { EmptyState } from '@/components/ui/EmptyState'; import { EmptyState } from '@/components/ui/EmptyState';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { X } from 'lucide-react';
import { import {
fetchTags, fetchTags,
assignTag, assignTag,
@@ -143,9 +144,7 @@ export function TagPicker({ entityType, entityId, assignedTags: initialAssigned
aria-label={t('tags.removeTag')} aria-label={t('tags.removeTag')}
disabled={submitting} disabled={submitting}
> >
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <X className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button> </button>
</div> </div>
))} ))}
+4 -7
View File
@@ -4,6 +4,7 @@ import { ChatWindow } from '@/components/ai/ChatWindow';
import { ResizablePanel } from '@/components/ui/ResizablePanel'; import { ResizablePanel } from '@/components/ui/ResizablePanel';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { fetchAgents, createSession, type AIAgent } from '@/api/ai'; import { fetchAgents, createSession, type AIAgent } from '@/api/ai';
import { ChevronLeft, Folder, Plus } from 'lucide-react';
export function AIAssistantPage() { export function AIAssistantPage() {
const [activeSessionId, setActiveSessionId] = useState<string | null>(null); const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
@@ -35,9 +36,7 @@ export function AIAssistantPage() {
type: 'button', type: 'button',
group: 'actions', group: 'actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Plus className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
), ),
onClick: async () => { onClick: async () => {
try { try {
@@ -57,9 +56,7 @@ export function AIAssistantPage() {
type: 'button', type: 'button',
group: 'actions', group: 'actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Folder className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
), ),
onClick: () => { onClick: () => {
const name = prompt('Ordnername:'); const name = prompt('Ordnername:');
@@ -106,7 +103,7 @@ export function AIAssistantPage() {
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<div className="h-14 flex items-center gap-2 px-3 border-b border-secondary-200 bg-white"> <div className="h-14 flex items-center gap-2 px-3 border-b border-secondary-200 bg-white">
<button onClick={() => setMobileView('list')} className="p-2 rounded-lg hover:bg-secondary-100" aria-label="Zurück"> <button onClick={() => setMobileView('list')} className="p-2 rounded-lg hover:bg-secondary-100" aria-label="Zurück">
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" /></svg> <ChevronLeft className="w-5 h-5" strokeWidth={2} />
</button> </button>
<span className="text-sm font-medium text-secondary-700 truncate">Chat</span> <span className="text-sm font-medium text-secondary-700 truncate">Chat</span>
</div> </div>
+9 -24
View File
@@ -24,6 +24,7 @@ import { IcsControls } from '@/components/calendar/IcsControls';
import { SharingSettings } from '@/components/calendar/SharingSettings'; import { SharingSettings } from '@/components/calendar/SharingSettings';
import { useCalendarStore, type CalendarViewMode } from '@/stores/calendarStore'; import { useCalendarStore, type CalendarViewMode } from '@/stores/calendarStore';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { ChevronLeft, ChevronRight, Info, Plus } from 'lucide-react';
import { import {
fetchCalendars, fetchCalendars,
createCalendar, createCalendar,
@@ -314,9 +315,7 @@ export function CalendarPage() {
: viewMode === 'day' ? goToPrevDay : viewMode === 'day' ? goToPrevDay
: () => {}, : () => {},
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ChevronLeft className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
), ),
}, },
{ {
@@ -340,9 +339,7 @@ export function CalendarPage() {
? goToNextDay ? goToNextDay
: () => {}, : () => {},
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ChevronRight className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
), ),
}, },
// View mode group // View mode group
@@ -374,9 +371,7 @@ export function CalendarPage() {
setModalOpen(true); setModalOpen(true);
}, },
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Plus className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
), ),
}, },
{ {
@@ -402,9 +397,7 @@ export function CalendarPage() {
active: showDetails, active: showDetails,
onClick: () => setShowDetails((v) => !v), onClick: () => setShowDetails((v) => !v),
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Info className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
), ),
}, },
]; ];
@@ -606,9 +599,7 @@ export function CalendarPage() {
data-testid="mobile-go-to-calendar" data-testid="mobile-go-to-calendar"
> >
<span className="text-sm font-medium">{t('calendar.title')}</span> <span className="text-sm font-medium">{t('calendar.title')}</span>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
</div> </div>
<CalendarTree <CalendarTree
@@ -633,9 +624,7 @@ export function CalendarPage() {
aria-label={t('common.back')} aria-label={t('common.back')}
data-testid="mobile-back-to-tree" data-testid="mobile-back-to-tree"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">{t('calendar.tree.title')}</span> <span className="text-sm font-medium">{t('calendar.tree.title')}</span>
</button> </button>
{showDetails && ( {showDetails && (
@@ -646,9 +635,7 @@ export function CalendarPage() {
data-testid="mobile-go-to-details" data-testid="mobile-go-to-details"
> >
<span className="text-sm font-medium">{t('calendar.detail.title')}</span> <span className="text-sm font-medium">{t('calendar.detail.title')}</span>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
)} )}
</div> </div>
@@ -667,9 +654,7 @@ export function CalendarPage() {
aria-label={t('common.back')} aria-label={t('common.back')}
data-testid="mobile-back-to-calendar" data-testid="mobile-back-to-calendar"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">{t('calendar.title')}</span> <span className="text-sm font-medium">{t('calendar.title')}</span>
</button> </button>
</div> </div>
+9 -24
View File
@@ -16,6 +16,7 @@ import { ContactFolderTree, type ContactFilter } from '@/components/contacts/Con
import { ContactList, type ContactViewMode } from '@/components/contacts/ContactList'; import { ContactList, type ContactViewMode } from '@/components/contacts/ContactList';
import { ContactDetail } from '@/components/contacts/ContactDetail'; import { ContactDetail } from '@/components/contacts/ContactDetail';
import { ContactEditModal } from '@/components/contacts/ContactEditModal'; import { ContactEditModal } from '@/components/contacts/ContactEditModal';
import { ArrowDownAZ, ArrowUpZA, ChevronLeft, LayoutGrid, List, Plus } from 'lucide-react';
import { import {
useUnifiedContacts, useUnifiedContacts,
useUnifiedContact, useUnifiedContact,
@@ -180,9 +181,7 @@ export function ContactsListPage() {
label: t('contacts.create'), label: t('contacts.create'),
group: 'actions', group: 'actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Plus className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
), ),
onClick: handleCreate, onClick: handleCreate,
}, },
@@ -259,9 +258,7 @@ export function ContactsListPage() {
aria-label={sortOrder === 'asc' ? t('common.sortDesc') : t('common.sortAsc')} aria-label={sortOrder === 'asc' ? t('common.sortDesc') : t('common.sortAsc')}
title={sortOrder === 'asc' ? 'Absteigend' : 'Aufsteigend'} title={sortOrder === 'asc' ? 'Absteigend' : 'Aufsteigend'}
> >
<svg className="w-3.5 h-3.5 text-secondary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> {sortOrder === 'asc' ? <ArrowDownAZ className="w-3.5 h-3.5 text-secondary-600" aria-hidden="true" strokeWidth={2} /> : <ArrowUpZA className="w-3.5 h-3.5 text-secondary-600" aria-hidden="true" strokeWidth={2} />}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortOrder === 'asc' ? 'M3 4h13M3 8h9M3 12h5m13 0l-4 4m4-4l-4-4m4 4v6' : 'M3 4h13M3 8h9M3 12h5m13-4v6m0 0l-4-4m4 4l-4 4'} />
</svg>
</button> </button>
</div> </div>
@@ -283,19 +280,13 @@ export function ContactsListPage() {
aria-pressed={viewMode === opt.value} aria-pressed={viewMode === opt.value}
> >
{opt.value === 'list' && ( {opt.value === 'list' && (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <List className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
)} )}
{opt.value === 'table' && ( {opt.value === 'table' && (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <LayoutGrid className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4h4v4H4V4zm6 0h4v4h-4V4zm6 0h4v4h-4V4zM4 10h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4zM4 16h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4z" />
</svg>
)} )}
{opt.value === 'cards' && ( {opt.value === 'cards' && (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <LayoutGrid className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5h6v6H4V5zm10 0h6v6h-6V5zM4 13h6v6H4v-6zm10 0h6v6h-6v-6z" />
</svg>
)} )}
</button> </button>
))} ))}
@@ -309,9 +300,7 @@ export function ContactsListPage() {
title={t('contacts.create')} title={t('contacts.create')}
data-testid="contacts-new-btn" data-testid="contacts-new-btn"
> >
<svg className="w-4 h-4 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Plus className="w-4 h-4 text-primary-600" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</button> </button>
</div> </div>
@@ -375,9 +364,7 @@ export function ContactsListPage() {
aria-label={t('common.back')} aria-label={t('common.back')}
data-testid="mobile-back-to-folders" data-testid="mobile-back-to-folders"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">{t('contacts.allContacts')}</span> <span className="text-sm font-medium">{t('contacts.allContacts')}</span>
</button> </button>
</div> </div>
@@ -417,9 +404,7 @@ export function ContactsListPage() {
aria-label={t('common.back')} aria-label={t('common.back')}
data-testid="mobile-back-to-list" data-testid="mobile-back-to-list"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">{t('common.back')}</span> <span className="text-sm font-medium">{t('common.back')}</span>
</button> </button>
</div> </div>
+9 -24
View File
@@ -21,6 +21,7 @@ import { FilePreviewModal } from '@/components/dms/FilePreviewModal';
import { ShareDialog } from '@/components/dms/ShareDialog'; import { ShareDialog } from '@/components/dms/ShareDialog';
import { BulkActions } from '@/components/dms/BulkActions'; import { BulkActions } from '@/components/dms/BulkActions';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { ArrowRight, ChevronLeft, ChevronRight, Info, Plus, Trash2, Upload } from 'lucide-react';
import { import {
fetchFolders, fetchFolders,
createFolder, createFolder,
@@ -371,9 +372,7 @@ export function DmsPage() {
label: t('dms.newFolder'), label: t('dms.newFolder'),
group: 'file-actions', group: 'file-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Plus className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
), ),
onClick: () => setShowNewFolder((v) => !v), onClick: () => setShowNewFolder((v) => !v),
}, },
@@ -383,9 +382,7 @@ export function DmsPage() {
label: t('dms.upload'), label: t('dms.upload'),
group: 'file-actions', group: 'file-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Upload className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
), ),
onClick: () => setShowUpload((v) => !v), onClick: () => setShowUpload((v) => !v),
}, },
@@ -426,9 +423,7 @@ export function DmsPage() {
group: 'details', group: 'details',
active: showDetails, active: showDetails,
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Info className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
), ),
onClick: () => setShowDetails((v) => !v), onClick: () => setShowDetails((v) => !v),
}, },
@@ -443,9 +438,7 @@ export function DmsPage() {
label: t('dms.bulkMove'), label: t('dms.bulkMove'),
group: 'bulk-actions', group: 'bulk-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ArrowRight className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
), ),
onClick: () => setShowBulkMove(true), onClick: () => setShowBulkMove(true),
}, },
@@ -455,9 +448,7 @@ export function DmsPage() {
label: t('dms.bulkDelete'), label: t('dms.bulkDelete'),
group: 'bulk-actions', group: 'bulk-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
), ),
onClick: handleBulkDelete, onClick: handleBulkDelete,
}, },
@@ -610,9 +601,7 @@ export function DmsPage() {
aria-label={t('common.back')} aria-label={t('common.back')}
data-testid="mobile-back-to-tree" data-testid="mobile-back-to-tree"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">{t('dms.sources')}</span> <span className="text-sm font-medium">{t('dms.sources')}</span>
</button> </button>
{showDetails && ( {showDetails && (
@@ -623,9 +612,7 @@ export function DmsPage() {
data-testid="mobile-go-to-details" data-testid="mobile-go-to-details"
> >
<span className="text-sm font-medium">{t('dms.details')}</span> <span className="text-sm font-medium">{t('dms.details')}</span>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button> </button>
)} )}
</div> </div>
@@ -659,9 +646,7 @@ export function DmsPage() {
aria-label={t('common.back')} aria-label={t('common.back')}
data-testid="mobile-back-to-files" data-testid="mobile-back-to-files"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">{t('dms.files')}</span> <span className="text-sm font-medium">{t('dms.files')}</span>
</button> </button>
</div> </div>
+2 -3
View File
@@ -13,6 +13,7 @@ import { EmptyState } from '@/components/ui/EmptyState';
import { Skeleton } from '@/components/ui/Skeleton'; import { Skeleton } from '@/components/ui/Skeleton';
import { useToast } from '@/components/ui/Toast'; import { useToast } from '@/components/ui/Toast';
import { restoreFile, type DmsFile } from '@/api/dms'; import { restoreFile, type DmsFile } from '@/api/dms';
import { Trash2 } from 'lucide-react';
export function DmsTrashPage() { export function DmsTrashPage() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -134,9 +135,7 @@ export function DmsTrashPage() {
<EmptyState <EmptyState
title={t('dms.trashEmpty')} title={t('dms.trashEmpty')}
icon={ icon={
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Trash2 className="w-12 h-12" aria-hidden="true" strokeWidth={1.5} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
} }
/> />
</Card> </Card>
+14 -40
View File
@@ -17,6 +17,7 @@ import { MailList } from '@/components/mail/MailList';
import { MailDetail } from '@/components/mail/MailDetail'; import { MailDetail } from '@/components/mail/MailDetail';
import { ComposeModal, type ComposeMode } from '@/components/mail/ComposeModal'; import { ComposeModal, type ComposeMode } from '@/components/mail/ComposeModal';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore'; import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { ArrowRight, Check, ChevronLeft, Loader2, Plus, Redo2, Trash2, TrendingUp, Undo2 } from 'lucide-react';
import { import {
fetchAccounts, fetchAccounts,
fetchFolders, fetchFolders,
@@ -536,9 +537,7 @@ export function MailPage() {
label: 'Verfassen', label: 'Verfassen',
group: 'compose', group: 'compose',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Plus className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
), ),
onClick: handleCompose, onClick: handleCompose,
}, },
@@ -549,9 +548,7 @@ export function MailPage() {
group: 'mail-actions', group: 'mail-actions',
disabled: !hasMail, disabled: !hasMail,
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Undo2 className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
), ),
onClick: () => selectedMail && handleReply(selectedMail), onClick: () => selectedMail && handleReply(selectedMail),
}, },
@@ -562,9 +559,7 @@ export function MailPage() {
group: 'mail-actions', group: 'mail-actions',
disabled: !hasMail, disabled: !hasMail,
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Redo2 className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6" />
</svg>
), ),
onClick: () => selectedMail && handleForward(selectedMail), onClick: () => selectedMail && handleForward(selectedMail),
}, },
@@ -594,9 +589,7 @@ export function MailPage() {
group: 'mail-actions', group: 'mail-actions',
disabled: !hasMail, disabled: !hasMail,
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
), ),
onClick: () => selectedMail && handleDeleteMail(selectedMail), onClick: () => selectedMail && handleDeleteMail(selectedMail),
}, },
@@ -607,9 +600,7 @@ export function MailPage() {
group: 'mail-actions', group: 'mail-actions',
disabled: !hasMail, disabled: !hasMail,
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ArrowRight className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
), ),
onClick: () => setShowMoveDropdown((v) => !v), onClick: () => setShowMoveDropdown((v) => !v),
}, },
@@ -620,9 +611,7 @@ export function MailPage() {
group: 'tools', group: 'tools',
disabled: isSyncing || !selectedAccountId, disabled: isSyncing || !selectedAccountId,
icon: ( icon: (
<svg className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Loader2 className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
), ),
onClick: async () => { onClick: async () => {
if (!selectedAccountId) return; if (!selectedAccountId) return;
@@ -652,9 +641,7 @@ export function MailPage() {
label: t('mail.markRead'), label: t('mail.markRead'),
group: 'bulk-actions', group: 'bulk-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Check className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
), ),
onClick: handleBulkMarkRead, onClick: handleBulkMarkRead,
}, },
@@ -664,9 +651,7 @@ export function MailPage() {
label: t('mail.markUnread'), label: t('mail.markUnread'),
group: 'bulk-actions', group: 'bulk-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <TrendingUp className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 19l6-6 4 4 8-8" />
</svg>
), ),
onClick: handleBulkMarkUnread, onClick: handleBulkMarkUnread,
}, },
@@ -676,9 +661,7 @@ export function MailPage() {
label: t('mail.bulkDelete'), label: t('mail.bulkDelete'),
group: 'bulk-actions', group: 'bulk-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
), ),
onClick: handleBulkDelete, onClick: handleBulkDelete,
}, },
@@ -688,9 +671,7 @@ export function MailPage() {
label: t('mail.bulkMove'), label: t('mail.bulkMove'),
group: 'bulk-actions', group: 'bulk-actions',
icon: ( icon: (
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <ArrowRight className="w-3.5 h-3.5" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
), ),
onClick: () => setShowMoveDropdown((v) => !v), onClick: () => setShowMoveDropdown((v) => !v),
}, },
@@ -703,10 +684,7 @@ export function MailPage() {
if (loadingAccounts) { if (loadingAccounts) {
return ( return (
<div className="flex items-center justify-center py-12" data-testid="mail-page-loading"> <div className="flex items-center justify-center py-12" data-testid="mail-page-loading">
<svg className="animate-spin h-6 w-6 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-6 w-6 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="ml-2 text-secondary-500">{t('common.loading')}</span> <span className="ml-2 text-secondary-500">{t('common.loading')}</span>
</div> </div>
); );
@@ -833,9 +811,7 @@ export function MailPage() {
aria-label="Zurück zu Ordnern" aria-label="Zurück zu Ordnern"
data-testid="mobile-back-to-folders" data-testid="mobile-back-to-folders"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">Ordner</span> <span className="text-sm font-medium">Ordner</span>
</button> </button>
</div> </div>
@@ -872,9 +848,7 @@ export function MailPage() {
aria-label="Zurück zur Liste" aria-label="Zurück zur Liste"
data-testid="mobile-back-to-list" data-testid="mobile-back-to-list"
> >
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
<span className="text-sm font-medium">Zurück</span> <span className="text-sm font-medium">Zurück</span>
</button> </button>
</div> </div>
+3 -7
View File
@@ -15,6 +15,7 @@ import { RuleEditor } from '@/components/mail/RuleEditor';
import { LabelManager } from '@/components/mail/LabelManager'; import { LabelManager } from '@/components/mail/LabelManager';
import { VacationResponder } from '@/components/mail/VacationResponder'; import { VacationResponder } from '@/components/mail/VacationResponder';
import { PgpSettings } from '@/components/mail/PgpSettings'; import { PgpSettings } from '@/components/mail/PgpSettings';
import { Loader2, Pencil } from 'lucide-react';
import { import {
fetchAccounts, fetchAccounts,
createAccount, createAccount,
@@ -291,10 +292,7 @@ export function MailSettingsPage() {
{loading ? ( {loading ? (
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true"> <Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
</div> </div>
) : accounts.length === 0 ? ( ) : accounts.length === 0 ? (
<p className="text-sm text-secondary-500">{t('mail.noAccounts')}</p> <p className="text-sm text-secondary-500">{t('mail.noAccounts')}</p>
@@ -324,9 +322,7 @@ export function MailSettingsPage() {
aria-label={t('common.edit')} aria-label={t('common.edit')}
data-testid={`edit-display-name-${acc.id}`} data-testid={`edit-display-name-${acc.id}`}
> >
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <Pencil className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button> </button>
</div> </div>
)} )}
+2 -3
View File
@@ -7,6 +7,7 @@ import { z } from 'zod';
import { usePasswordResetRequest } from '@/api/hooks'; import { usePasswordResetRequest } from '@/api/hooks';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Check } from 'lucide-react';
const resetRequestSchema = z.object({ const resetRequestSchema = z.object({
email: z.string().email(), email: z.string().email(),
@@ -43,9 +44,7 @@ export function PasswordResetRequestPage() {
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<div className="bg-white rounded-lg shadow-md p-8 text-center"> <div className="bg-white rounded-lg shadow-md p-8 text-center">
<div className="mb-4 w-12 h-12 mx-auto rounded-full bg-success-100 flex items-center justify-center" aria-hidden="true"> <div className="mb-4 w-12 h-12 mx-auto rounded-full bg-success-100 flex items-center justify-center" aria-hidden="true">
<svg className="w-6 h-6 text-success-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <Check className="w-6 h-6 text-success-600" strokeWidth={2} />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div> </div>
<p className="text-secondary-700 mb-6">{t('auth.resetRequestSuccess')}</p> <p className="text-secondary-700 mb-6">{t('auth.resetRequestSuccess')}</p>
<Link to="/login" className="text-sm text-primary-600 hover:text-primary-700"> <Link to="/login" className="text-sm text-primary-600 hover:text-primary-700">