Phase 0.2: migrate remaining SVGs to lucide-react icons
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
} from '@/api/calendar';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Calendar as CalendarIcon, Pencil, Trash2, X } from 'lucide-react';
|
||||
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '-';
|
||||
@@ -139,9 +140,7 @@ export function CalendarDetail({
|
||||
className="flex flex-col items-center justify-center h-full p-6 text-center"
|
||||
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">
|
||||
<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>
|
||||
<CalendarIcon className="w-12 h-12 text-secondary-300 mb-3" aria-hidden="true" strokeWidth={1.5} />
|
||||
<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>
|
||||
</div>
|
||||
@@ -163,9 +162,7 @@ export function CalendarDetail({
|
||||
aria-label={t('common.close')}
|
||||
data-testid="calendar-detail-close"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</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"
|
||||
data-testid="calendar-detail-edit"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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>
|
||||
<Pencil className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('calendar.detail.edit')}
|
||||
</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"
|
||||
data-testid="calendar-detail-delete"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('calendar.detail.delete')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import React, { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { Calendar, CalendarType } from '@/api/calendar';
|
||||
import { ChevronDown, Plus } from 'lucide-react';
|
||||
|
||||
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"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<span className="truncate text-secondary-700">{t(`calendar.type.${type}`)}</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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('calendar.tree.newCalendar')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { MessageBlock } from '@/store/commStore';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
interface ContactCardBlockProps {
|
||||
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-xs text-secondary-400">Kontakt anzeigen</p>
|
||||
</div>
|
||||
<svg
|
||||
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>
|
||||
<ChevronRight className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import type { MessageBlock } from '@/store/commStore';
|
||||
import { Download, FileText } from 'lucide-react';
|
||||
|
||||
interface FileBlockProps {
|
||||
block: MessageBlock;
|
||||
@@ -14,37 +15,11 @@ function formatFileSize(bytes: number | undefined): string {
|
||||
}
|
||||
|
||||
const fileIcon = (
|
||||
<svg
|
||||
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>
|
||||
<FileText className="w-8 h-8 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
|
||||
);
|
||||
|
||||
const downloadIcon = (
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
<Download className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const FileBlock: React.FC<FileBlockProps> = ({ block }) => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import type { MessageBlock } from '@/store/commStore';
|
||||
|
||||
import { AppWindow } from 'lucide-react';
|
||||
|
||||
interface MiniAppBlockProps {
|
||||
block: MessageBlock;
|
||||
}
|
||||
@@ -14,20 +16,7 @@ const MiniAppBlock: React.FC<MiniAppBlockProps> = ({ block }) => {
|
||||
return (
|
||||
<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">
|
||||
<svg
|
||||
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>
|
||||
<AppWindow className="w-8 h-8 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="text-sm font-medium text-secondary-600">
|
||||
Mini-App: {appId}
|
||||
</p>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
type UnifiedContact,
|
||||
type ContactPerson,
|
||||
@@ -145,10 +147,7 @@ export function ContactDetail({ contact, loading, onEdit, onDeleted }: ContactDe
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useMoveContactToFolder,
|
||||
} from '@/api/hooks';
|
||||
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}`;
|
||||
|
||||
@@ -22,28 +23,21 @@ export interface ContactFolderTreeProps {
|
||||
|
||||
// Icons
|
||||
|
||||
const icon = (path: string, cls = 'w-4 h-4') => (
|
||||
<svg className={cls} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={path} />
|
||||
</svg>
|
||||
const icon = (IconComp: React.ElementType, cls = 'w-4 h-4') => (
|
||||
<IconComp className={cls} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const chevron = (open: boolean) => (
|
||||
<svg
|
||||
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>
|
||||
<ChevronRight className={clsx('w-3.5 h-3.5 transition-transform flex-shrink-0', open && 'rotate-90')} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
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',
|
||||
folder: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z',
|
||||
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',
|
||||
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',
|
||||
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',
|
||||
plus: 'M12 4v16m8-8H4',
|
||||
all: Users,
|
||||
folder: Folder,
|
||||
tag: Tag,
|
||||
edit: Pencil,
|
||||
trash: Trash2,
|
||||
plus: Plus,
|
||||
};
|
||||
|
||||
// Context Menu
|
||||
|
||||
@@ -6,6 +6,8 @@ import { Pagination } from '@/components/ui/Pagination';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import type { UnifiedContact } from '@/api/hooks';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export type ContactViewMode = 'list' | 'table' | 'cards';
|
||||
|
||||
export interface ContactListProps {
|
||||
@@ -77,10 +79,7 @@ export function ContactList({
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFile } from '@/api/dms';
|
||||
import { getFileIcon, formatFileSize } from './FileExplorer';
|
||||
import { Download, Eye, FileText, Share2, Trash2, X } from 'lucide-react';
|
||||
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return '-';
|
||||
@@ -37,9 +38,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
if (!file) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<FileText className="w-12 h-12 text-secondary-300 mb-3" aria-hidden="true" strokeWidth={1.5} />
|
||||
<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>
|
||||
</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"
|
||||
aria-label={t('common.close')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -82,9 +79,7 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<svg className={clsx('w-24 h-24', icon.color)} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-24 h-24', icon.color)} aria-hidden="true" strokeWidth={1} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -97,19 +92,14 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 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>
|
||||
<Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.preview')}
|
||||
</button>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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>
|
||||
<Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.share')}
|
||||
</button>
|
||||
<a
|
||||
@@ -117,18 +107,14 @@ export function FileDetails({ file, onPreview, onShare, onDelete, onClose }: Fil
|
||||
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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
<Download className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.download')}
|
||||
</a>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
{t('dms.delete')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -7,27 +7,28 @@ import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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') {
|
||||
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/')) {
|
||||
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')) {
|
||||
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')) {
|
||||
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')) {
|
||||
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')) {
|
||||
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 {
|
||||
@@ -101,9 +102,7 @@ function SortHeader({
|
||||
>
|
||||
{label}
|
||||
{isActive && (
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={sortOrder === 'asc' ? 'M5 15l7-7 7 7' : 'M19 9l-7 7-7-7'} />
|
||||
</svg>
|
||||
sortOrder === 'asc' ? <ChevronUp className="w-3 h-3" aria-hidden="true" strokeWidth={2} /> : <ChevronDown className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
@@ -130,10 +129,7 @@ function ActionButtons({
|
||||
aria-label={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">
|
||||
<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>
|
||||
<Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileShare(file); }}
|
||||
@@ -141,9 +137,7 @@ function ActionButtons({
|
||||
aria-label={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">
|
||||
<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>
|
||||
<Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileDelete(file); }}
|
||||
@@ -151,9 +145,7 @@ function ActionButtons({
|
||||
aria-label={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">
|
||||
<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>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@@ -198,10 +190,7 @@ export function FileExplorer({
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-6 w-6 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-secondary-500 text-sm">{t('dms.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -210,9 +199,7 @@ export function FileExplorer({
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -254,9 +241,7 @@ export function FileExplorer({
|
||||
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500 flex-shrink-0"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-5 h-5 flex-shrink-0', icon.color)} aria-hidden="true" strokeWidth={1.5} />
|
||||
<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-400 flex-shrink-0 hidden sm:inline">{formatDate(file.updated_at || file.created_at)}</span>
|
||||
@@ -327,9 +312,7 @@ export function FileExplorer({
|
||||
</td>
|
||||
<td className="px-3 py-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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-5 h-5 flex-shrink-0', icon.color)} aria-hidden="true" strokeWidth={1.5} />
|
||||
<span className="font-medium text-secondary-900 truncate" title={file.name}>{file.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
@@ -415,9 +398,7 @@ export function FileExplorer({
|
||||
/>
|
||||
</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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx(iconSize, icon.color, isSmall ? 'mb-0.5' : 'mb-1')} aria-hidden="true" strokeWidth={1.5} />
|
||||
)}
|
||||
<p className={clsx('font-medium text-secondary-900 truncate w-full', isSmall ? 'text-[10px]' : 'text-xs')} title={file.name}>{file.name}</p>
|
||||
{!isSmall && (
|
||||
|
||||
@@ -7,35 +7,9 @@ import React, { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFile } from '@/api/dms';
|
||||
import { Eye, FileText, Share2, Trash2 } from 'lucide-react';
|
||||
|
||||
function getFileIcon(mimeType: string): { path: string; color: string } {
|
||||
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`;
|
||||
}
|
||||
import { getFileIcon, formatFileSize } from "./FileExplorer";
|
||||
|
||||
export interface FileGridProps {
|
||||
files: DmsFile[];
|
||||
@@ -81,9 +55,7 @@ export function FileGrid({
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.noFiles')}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -114,9 +86,7 @@ export function FileGrid({
|
||||
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d={icon.path} />
|
||||
</svg>
|
||||
<icon.icon className={clsx('w-10 h-10', icon.color)} aria-hidden="true" strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
<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')}
|
||||
title={t('dms.preview')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 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>
|
||||
<Eye className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileShare(file); }}
|
||||
@@ -142,9 +109,7 @@ export function FileGrid({
|
||||
aria-label={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">
|
||||
<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>
|
||||
<Share2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); onFileDelete(file); }}
|
||||
@@ -152,9 +117,7 @@ export function FileGrid({
|
||||
aria-label={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">
|
||||
<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>
|
||||
<Trash2 className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Modal } from '@/components/ui/Modal';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { getFilePreviewUrl, type DmsFile } from '@/api/dms';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
export interface FilePreviewModalProps {
|
||||
open: boolean;
|
||||
@@ -81,9 +82,7 @@ export function FilePreviewModal({ open, file, onClose }: FilePreviewModalProps)
|
||||
|
||||
{!isPdf && !isImage && (
|
||||
<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">
|
||||
<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>
|
||||
<FileText className="mx-auto h-12 w-12 text-secondary-300" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-500">{t('dms.preview')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFolder } from '@/api/dms';
|
||||
import { ChevronDown, Folder, Menu } from 'lucide-react';
|
||||
|
||||
interface FolderTreeItemProps {
|
||||
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"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
{!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">
|
||||
<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>
|
||||
<Folder className="w-4 h-4 text-warning-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
{typeof folder.file_count === 'number' && folder.file_count > 0 && (
|
||||
<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"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<Menu className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
<span>{t('dms.allFiles')}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -12,6 +12,7 @@ import React, { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DmsFolder, DmsFile } from '@/api/dms';
|
||||
import { ChevronDown, FileText, Folder, Home, Menu, Users } from 'lucide-react';
|
||||
|
||||
interface SourceTreeFolderItemProps {
|
||||
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"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
{!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">
|
||||
<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>
|
||||
<Folder className={clsx('w-4 h-4 flex-shrink-0', isDropTarget ? 'text-primary-500' : 'text-warning-500')} aria-hidden="true" strokeWidth={2} />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
{typeof folder.file_count === 'number' && folder.file_count > 0 && (
|
||||
<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"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={expanded ? 'M19 9l-7 7-7-7' : 'M9 5l7 7-7 7'} />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 transition-transform" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
{icon}
|
||||
<span className="truncate">{title}</span>
|
||||
@@ -298,9 +293,7 @@ export function SourceTree({
|
||||
role="button"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<Menu className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
<span>{t('dms.allFiles')}</span>
|
||||
</div>
|
||||
|
||||
@@ -315,9 +308,7 @@ export function SourceTree({
|
||||
}
|
||||
}}
|
||||
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">
|
||||
<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>
|
||||
<Folder className="w-4 h-4 text-primary-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
{folders.length === 0 ? (
|
||||
@@ -346,9 +337,7 @@ export function SourceTree({
|
||||
onSelectFolder(null);
|
||||
}}
|
||||
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">
|
||||
<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>
|
||||
<Users className="w-4 h-4 text-accent-500 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
{sharedFiles.length === 0 ? (
|
||||
@@ -367,9 +356,7 @@ export function SourceTree({
|
||||
role="button"
|
||||
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">
|
||||
<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>
|
||||
<FileText className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="truncate">{file.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
@@ -386,9 +373,7 @@ export function SourceTree({
|
||||
onSelectFolder(null);
|
||||
}}
|
||||
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">
|
||||
<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>
|
||||
<Home className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
defaultExpanded={false}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { uploadFile } from '@/api/dms';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { Upload } from 'lucide-react';
|
||||
|
||||
export interface UploadDropzoneProps {
|
||||
folderId: string | null;
|
||||
@@ -124,9 +125,7 @@ export function UploadDropzone({ folderId, onUploaded }: UploadDropzoneProps) {
|
||||
className="hidden"
|
||||
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">
|
||||
<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>
|
||||
<Upload className="mx-auto h-10 w-10 text-secondary-400" aria-hidden="true" strokeWidth={1.5} />
|
||||
<p className="mt-2 text-sm text-secondary-600">{t('dms.uploadDropHere')}</p>
|
||||
</div>
|
||||
{uploads.length > 0 && (
|
||||
|
||||
@@ -7,41 +7,30 @@ import { useUIStore } from '@/store/uiStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUsers, useGroups } from '@/api/hooks';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { Bell, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Users, X } from 'lucide-react';
|
||||
|
||||
const robotIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Bot className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const bellIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Bell className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const bulbIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Lightbulb className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const teamIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Users className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const chatBubbleIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<MessageSquare className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const chevronRightIcon = (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
interface TabDef {
|
||||
@@ -94,9 +83,7 @@ function TeamPanel() {
|
||||
{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 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">
|
||||
<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>
|
||||
<Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<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"
|
||||
onClick={() => removeNotification(i)}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
@@ -283,9 +268,7 @@ export function AISidebar() {
|
||||
aria-label="Zurück"
|
||||
data-testid="ai-sidebar-back"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-5 h-5" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Zurück</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,55 +15,40 @@ import {
|
||||
import { useCommWebSocket } from '@/hooks/useCommWebSocket';
|
||||
import type { Conversation, Message } from '@/store/commStore';
|
||||
import { BlockRenderer } from '@/components/comm/blocks';
|
||||
import { Bell, Bookmark, Bot, ChevronLeft, ChevronRight, Lightbulb, MessageSquare, Send, Users } from 'lucide-react';
|
||||
|
||||
// ─── Icons (identical to AISidebar) ───
|
||||
|
||||
const robotIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Bot className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const bellIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Bell className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const bulbIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Lightbulb className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const teamIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Users className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const chatBubbleIcon = (className: string) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<MessageSquare className={className} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const chevronRightIcon = (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const sendIcon = (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
<Send className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
const pinIcon = (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
|
||||
</svg>
|
||||
<Bookmark className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
// ─── Types ───
|
||||
@@ -128,9 +113,7 @@ function TeamPanel({ onStartDirectChat }: { onStartDirectChat: (userId: string,
|
||||
{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 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">
|
||||
<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>
|
||||
<Users className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-secondary-700 truncate">{g.name}</p>
|
||||
@@ -660,9 +643,7 @@ export function MessageSidebar() {
|
||||
aria-label="Zurück"
|
||||
data-testid="message-sidebar-back"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-5 h-5" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Zurück</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useUIStore } from '@/store/uiStore';
|
||||
import { Calendar, ChevronRight, FileText, Home, Mail, Monitor, Users } from 'lucide-react';
|
||||
|
||||
interface NavLeaf {
|
||||
to: string;
|
||||
@@ -21,33 +22,19 @@ interface NavSingleItem {
|
||||
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) => (
|
||||
<svg
|
||||
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>
|
||||
<ChevronRight className={clsx('w-4 h-4 flex-shrink-0 transition-transform duration-200', expanded ? 'rotate-90' : '')} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
|
||||
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: '/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: '/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: <Users className="w-5 h-5 flex-shrink-0" aria-hidden="true" strokeWidth={2} /> },
|
||||
];
|
||||
|
||||
const treeItems: NavTreeItem[] = [
|
||||
{
|
||||
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: [
|
||||
{ to: '/calendar', labelKey: 'nav.calendar' },
|
||||
{ to: '/calendar/kanban', labelKey: 'nav.calendarKanban' },
|
||||
@@ -55,7 +42,7 @@ const treeItems: NavTreeItem[] = [
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{ to: '/dms', labelKey: 'nav.files' },
|
||||
{ to: '/dms/trash', labelKey: 'nav.filesTrash' },
|
||||
@@ -63,7 +50,7 @@ const treeItems: NavTreeItem[] = [
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{ to: '/mail', labelKey: 'nav.email' },
|
||||
{ to: '/mail/settings', labelKey: 'nav.emailSettings' },
|
||||
@@ -72,7 +59,7 @@ const treeItems: NavTreeItem[] = [
|
||||
];
|
||||
|
||||
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() {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useLogout } from '@/api/hooks';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { SearchDropdown } from '@/components/shared/SearchDropdown';
|
||||
import { SuggestionBadge } from '@/components/ai/SuggestionBadge';
|
||||
import { Building, ChevronDown, Menu } from 'lucide-react';
|
||||
|
||||
export function TopBar() {
|
||||
const { t } = useTranslation();
|
||||
@@ -52,9 +53,7 @@ export function TopBar() {
|
||||
aria-label="Seitenleiste ein-/ausklappen"
|
||||
aria-expanded={true}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<Menu className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
{/* 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"
|
||||
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">
|
||||
<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>
|
||||
<Building className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
{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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<ChevronDown className="w-3 h-3 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -95,9 +90,7 @@ export function TopBar() {
|
||||
<span className="hidden md:block text-sm font-medium text-secondary-700 max-w-24 truncate">
|
||||
{user?.first_name || ''}
|
||||
</span>
|
||||
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
<ChevronDown className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
{userMenuOpen && (
|
||||
<div
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RichTextEditor } from './RichTextEditor';
|
||||
import type { Mail, MailSignature, SendMailPayload, ReplyPayload, ForwardPayload, MailDraftPayload } from '@/api/mail';
|
||||
import { uploadAttachment, type UploadedAttachment, replaceSignatureVariables } from '@/api/mail';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { FileText, Paperclip, X } from 'lucide-react';
|
||||
|
||||
export type ComposeMode = 'new' | 'reply' | 'forward' | 'draft';
|
||||
|
||||
@@ -309,9 +310,7 @@ export function ComposeModal({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
isLoading={uploadingFile}
|
||||
icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.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>
|
||||
<Paperclip className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
{t('mail.addAttachment')}
|
||||
@@ -322,9 +321,7 @@ export function ComposeModal({
|
||||
<ul className="mt-2 space-y-1">
|
||||
{attachments.map((att) => (
|
||||
<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">
|
||||
<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>
|
||||
<FileText className="w-5 h-5 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-secondary-800 truncate">{att.filename}</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"
|
||||
aria-label={t('common.remove')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { Loader2, X } from 'lucide-react';
|
||||
import {
|
||||
fetchLabels,
|
||||
createLabel,
|
||||
@@ -81,10 +82,7 @@ export function LabelManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -155,9 +153,7 @@ export function LabelManager() {
|
||||
data-testid={`delete-label-${label.id}`}
|
||||
type="button"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Mail, MailAttachment } from '@/api/mail';
|
||||
import { decodeMimeHeader } from '@/api/mail';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { FileText, Loader2 } from 'lucide-react';
|
||||
|
||||
export interface MailDetailProps {
|
||||
mail: Mail | null; loading: boolean;
|
||||
@@ -75,10 +76,7 @@ export function MailDetail({
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -158,9 +156,7 @@ export function MailDetail({
|
||||
<ul className="space-y-1 md:space-y-2">
|
||||
{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">
|
||||
<svg className="w-5 h-5 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 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>
|
||||
<FileText className="w-5 h-5 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-secondary-800 truncate">{decodeMimeHeader(att.filename)}</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 { Badge } from '@/components/ui/Badge';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { ChevronRight, Folder, Loader2 } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Map IMAP folder names to German display names.
|
||||
@@ -88,23 +89,13 @@ function buildFolderTree(folders: MailFolder[], accountId: string): MailFolder[]
|
||||
|
||||
function ChevronIcon({ expanded }: { expanded: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
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>
|
||||
<ChevronRight className={clsx('w-4 h-4 transition-transform flex-shrink-0', expanded && 'rotate-90')} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
|
||||
function FolderIcon() {
|
||||
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">
|
||||
<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>
|
||||
<Folder className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,10 +295,7 @@ export function MailFolderTree({ accounts, folders, selectedFolderId, onSelect,
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Mail } from '@/api/mail';
|
||||
import { decodeMimeHeader } from '@/api/mail';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
import { ChevronUp, Loader2, Paperclip, Star } from 'lucide-react';
|
||||
|
||||
export interface MailListProps {
|
||||
mails: Mail[];
|
||||
@@ -70,10 +71,7 @@ export function MailList({
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -132,11 +130,7 @@ export function MailList({
|
||||
title={sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}
|
||||
data-testid="mail-sort-order"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
{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>
|
||||
<ChevronUp className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
|
||||
<span>{sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -180,16 +174,12 @@ export function MailList({
|
||||
{mail.flag_type === 'important' && <span className="text-sm">❗</span>}
|
||||
{mail.flag_type === 'question' && <span className="text-sm">❓</span>}
|
||||
{(!mail.flag_type || mail.flag_type === '') && (
|
||||
<svg className="w-4 h-4 text-warning-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
<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>
|
||||
<Star className="w-4 h-4 text-warning-500" fill="currentColor" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{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">
|
||||
<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>
|
||||
<Paperclip className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
export interface MailSearchBarProps {
|
||||
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"
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<Search className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Input } from '@/components/ui/Input';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
importPgpKey,
|
||||
fetchPgpKeys,
|
||||
@@ -83,10 +85,7 @@ export function PgpSettings() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
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 {
|
||||
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">
|
||||
{/* Undo / Redo */}
|
||||
<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 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>
|
||||
|
||||
<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 */}
|
||||
<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 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 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 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>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* Lists */}
|
||||
<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 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 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 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 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>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* Alignment */}
|
||||
<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 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 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 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>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* 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
|
||||
type="color"
|
||||
className="sr-only"
|
||||
@@ -208,17 +209,17 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
|
||||
|
||||
{/* Link & Image */}
|
||||
<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 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>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
fetchRules,
|
||||
createRule,
|
||||
@@ -132,10 +134,7 @@ export function RuleEditor({ accountId }: { accountId: string }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { RichTextEditor } from './RichTextEditor';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
fetchSignatures,
|
||||
createSignature,
|
||||
@@ -112,10 +114,7 @@ export function SignatureManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { fetchTemplates, substituteTemplate, type MailTemplate } from '@/api/mail';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export interface TemplatePickerProps {
|
||||
onSelect: (body: string, subject: string) => void;
|
||||
}
|
||||
@@ -55,10 +57,7 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,8 @@ import clsx from 'clsx';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export interface DataGridProps<T> {
|
||||
columns: ColumnDef<T, any>[];
|
||||
data: T[];
|
||||
@@ -107,10 +109,7 @@ export function DataGrid<T extends Record<string, any>>({
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="px-6 py-8 text-center text-secondary-500">
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin motion-reduce:animate-none h-5 w-5" aria-hidden="true" />
|
||||
{t('common.loading')}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGlobalSearch, SearchResult } from '@/api/hooks';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
export interface SearchDropdownProps {
|
||||
placeholder?: string;
|
||||
@@ -96,9 +97,7 @@ export function SearchDropdown({ placeholder }: SearchDropdownProps) {
|
||||
aria-controls="search-results-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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
{open && query.trim() && (
|
||||
<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"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { X } from 'lucide-react';
|
||||
import {
|
||||
fetchTags,
|
||||
assignTag,
|
||||
@@ -143,9 +144,7 @@ export function TagPicker({ entityType, entityId, assignedTags: initialAssigned
|
||||
aria-label={t('tags.removeTag')}
|
||||
disabled={submitting}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ChatWindow } from '@/components/ai/ChatWindow';
|
||||
import { ResizablePanel } from '@/components/ui/ResizablePanel';
|
||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||
import { fetchAgents, createSession, type AIAgent } from '@/api/ai';
|
||||
import { ChevronLeft, Folder, Plus } from 'lucide-react';
|
||||
|
||||
export function AIAssistantPage() {
|
||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||
@@ -35,9 +36,7 @@ export function AIAssistantPage() {
|
||||
type: 'button',
|
||||
group: 'actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: async () => {
|
||||
try {
|
||||
@@ -57,9 +56,7 @@ export function AIAssistantPage() {
|
||||
type: 'button',
|
||||
group: 'actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Folder className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => {
|
||||
const name = prompt('Ordnername:');
|
||||
@@ -106,7 +103,7 @@ export function AIAssistantPage() {
|
||||
<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">
|
||||
<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>
|
||||
<span className="text-sm font-medium text-secondary-700 truncate">Chat</span>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import { IcsControls } from '@/components/calendar/IcsControls';
|
||||
import { SharingSettings } from '@/components/calendar/SharingSettings';
|
||||
import { useCalendarStore, type CalendarViewMode } from '@/stores/calendarStore';
|
||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||
import { ChevronLeft, ChevronRight, Info, Plus } from 'lucide-react';
|
||||
import {
|
||||
fetchCalendars,
|
||||
createCalendar,
|
||||
@@ -314,9 +315,7 @@ export function CalendarPage() {
|
||||
: viewMode === 'day' ? goToPrevDay
|
||||
: () => {},
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.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-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -340,9 +339,7 @@ export function CalendarPage() {
|
||||
? goToNextDay
|
||||
: () => {},
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
},
|
||||
// View mode group
|
||||
@@ -374,9 +371,7 @@ export function CalendarPage() {
|
||||
setModalOpen(true);
|
||||
},
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -402,9 +397,7 @@ export function CalendarPage() {
|
||||
active: showDetails,
|
||||
onClick: () => setShowDetails((v) => !v),
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Info className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -606,9 +599,7 @@ export function CalendarPage() {
|
||||
data-testid="mobile-go-to-calendar"
|
||||
>
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
<CalendarTree
|
||||
@@ -633,9 +624,7 @@ export function CalendarPage() {
|
||||
aria-label={t('common.back')}
|
||||
data-testid="mobile-back-to-tree"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">{t('calendar.tree.title')}</span>
|
||||
</button>
|
||||
{showDetails && (
|
||||
@@ -646,9 +635,7 @@ export function CalendarPage() {
|
||||
data-testid="mobile-go-to-details"
|
||||
>
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -667,9 +654,7 @@ export function CalendarPage() {
|
||||
aria-label={t('common.back')}
|
||||
data-testid="mobile-back-to-calendar"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">{t('calendar.title')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ContactFolderTree, type ContactFilter } from '@/components/contacts/Con
|
||||
import { ContactList, type ContactViewMode } from '@/components/contacts/ContactList';
|
||||
import { ContactDetail } from '@/components/contacts/ContactDetail';
|
||||
import { ContactEditModal } from '@/components/contacts/ContactEditModal';
|
||||
import { ArrowDownAZ, ArrowUpZA, ChevronLeft, LayoutGrid, List, Plus } from 'lucide-react';
|
||||
import {
|
||||
useUnifiedContacts,
|
||||
useUnifiedContact,
|
||||
@@ -180,9 +181,7 @@ export function ContactsListPage() {
|
||||
label: t('contacts.create'),
|
||||
group: 'actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: handleCreate,
|
||||
},
|
||||
@@ -259,9 +258,7 @@ export function ContactsListPage() {
|
||||
aria-label={sortOrder === 'asc' ? t('common.sortDesc') : t('common.sortAsc')}
|
||||
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">
|
||||
<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>
|
||||
{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} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -283,19 +280,13 @@ export function ContactsListPage() {
|
||||
aria-pressed={viewMode === opt.value}
|
||||
>
|
||||
{opt.value === 'list' && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
<List className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
{opt.value === 'table' && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4h4v4H4V4zm6 0h4v4h-4V4zm6 0h4v4h-4V4zM4 10h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4zM4 16h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4z" />
|
||||
</svg>
|
||||
<LayoutGrid className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
{opt.value === 'cards' && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5h6v6H4V5zm10 0h6v6h-6V5zM4 13h6v6H4v-6zm10 0h6v6h-6v-6z" />
|
||||
</svg>
|
||||
<LayoutGrid className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -309,9 +300,7 @@ export function ContactsListPage() {
|
||||
title={t('contacts.create')}
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-4 h-4 text-primary-600" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -375,9 +364,7 @@ export function ContactsListPage() {
|
||||
aria-label={t('common.back')}
|
||||
data-testid="mobile-back-to-folders"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">{t('contacts.allContacts')}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -417,9 +404,7 @@ export function ContactsListPage() {
|
||||
aria-label={t('common.back')}
|
||||
data-testid="mobile-back-to-list"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">{t('common.back')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { FilePreviewModal } from '@/components/dms/FilePreviewModal';
|
||||
import { ShareDialog } from '@/components/dms/ShareDialog';
|
||||
import { BulkActions } from '@/components/dms/BulkActions';
|
||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||
import { ArrowRight, ChevronLeft, ChevronRight, Info, Plus, Trash2, Upload } from 'lucide-react';
|
||||
import {
|
||||
fetchFolders,
|
||||
createFolder,
|
||||
@@ -371,9 +372,7 @@ export function DmsPage() {
|
||||
label: t('dms.newFolder'),
|
||||
group: 'file-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => setShowNewFolder((v) => !v),
|
||||
},
|
||||
@@ -383,9 +382,7 @@ export function DmsPage() {
|
||||
label: t('dms.upload'),
|
||||
group: 'file-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Upload className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => setShowUpload((v) => !v),
|
||||
},
|
||||
@@ -426,9 +423,7 @@ export function DmsPage() {
|
||||
group: 'details',
|
||||
active: showDetails,
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Info className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => setShowDetails((v) => !v),
|
||||
},
|
||||
@@ -443,9 +438,7 @@ export function DmsPage() {
|
||||
label: t('dms.bulkMove'),
|
||||
group: 'bulk-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
<ArrowRight className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => setShowBulkMove(true),
|
||||
},
|
||||
@@ -455,9 +448,7 @@ export function DmsPage() {
|
||||
label: t('dms.bulkDelete'),
|
||||
group: 'bulk-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: handleBulkDelete,
|
||||
},
|
||||
@@ -610,9 +601,7 @@ export function DmsPage() {
|
||||
aria-label={t('common.back')}
|
||||
data-testid="mobile-back-to-tree"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">{t('dms.sources')}</span>
|
||||
</button>
|
||||
{showDetails && (
|
||||
@@ -623,9 +612,7 @@ export function DmsPage() {
|
||||
data-testid="mobile-go-to-details"
|
||||
>
|
||||
<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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -659,9 +646,7 @@ export function DmsPage() {
|
||||
aria-label={t('common.back')}
|
||||
data-testid="mobile-back-to-files"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">{t('dms.files')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Skeleton } from '@/components/ui/Skeleton';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { restoreFile, type DmsFile } from '@/api/dms';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
|
||||
export function DmsTrashPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -134,9 +135,7 @@ export function DmsTrashPage() {
|
||||
<EmptyState
|
||||
title={t('dms.trashEmpty')}
|
||||
icon={
|
||||
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<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>
|
||||
<Trash2 className="w-12 h-12" aria-hidden="true" strokeWidth={1.5} />
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
+14
-40
@@ -17,6 +17,7 @@ import { MailList } from '@/components/mail/MailList';
|
||||
import { MailDetail } from '@/components/mail/MailDetail';
|
||||
import { ComposeModal, type ComposeMode } from '@/components/mail/ComposeModal';
|
||||
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
|
||||
import { ArrowRight, Check, ChevronLeft, Loader2, Plus, Redo2, Trash2, TrendingUp, Undo2 } from 'lucide-react';
|
||||
import {
|
||||
fetchAccounts,
|
||||
fetchFolders,
|
||||
@@ -536,9 +537,7 @@ export function MailPage() {
|
||||
label: 'Verfassen',
|
||||
group: 'compose',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: handleCompose,
|
||||
},
|
||||
@@ -549,9 +548,7 @@ export function MailPage() {
|
||||
group: 'mail-actions',
|
||||
disabled: !hasMail,
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" 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-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => selectedMail && handleReply(selectedMail),
|
||||
},
|
||||
@@ -562,9 +559,7 @@ export function MailPage() {
|
||||
group: 'mail-actions',
|
||||
disabled: !hasMail,
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" 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-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => selectedMail && handleForward(selectedMail),
|
||||
},
|
||||
@@ -594,9 +589,7 @@ export function MailPage() {
|
||||
group: 'mail-actions',
|
||||
disabled: !hasMail,
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => selectedMail && handleDeleteMail(selectedMail),
|
||||
},
|
||||
@@ -607,9 +600,7 @@ export function MailPage() {
|
||||
group: 'mail-actions',
|
||||
disabled: !hasMail,
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
<ArrowRight className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => setShowMoveDropdown((v) => !v),
|
||||
},
|
||||
@@ -620,9 +611,7 @@ export function MailPage() {
|
||||
group: 'tools',
|
||||
disabled: isSyncing || !selectedAccountId,
|
||||
icon: (
|
||||
<svg className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Loader2 className={`w-3.5 h-3.5 ${isSyncing ? 'animate-spin' : ''}`} strokeWidth={2} />
|
||||
),
|
||||
onClick: async () => {
|
||||
if (!selectedAccountId) return;
|
||||
@@ -652,9 +641,7 @@ export function MailPage() {
|
||||
label: t('mail.markRead'),
|
||||
group: 'bulk-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<Check className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: handleBulkMarkRead,
|
||||
},
|
||||
@@ -664,9 +651,7 @@ export function MailPage() {
|
||||
label: t('mail.markUnread'),
|
||||
group: 'bulk-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 19l6-6 4 4 8-8" />
|
||||
</svg>
|
||||
<TrendingUp className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: handleBulkMarkUnread,
|
||||
},
|
||||
@@ -676,9 +661,7 @@ export function MailPage() {
|
||||
label: t('mail.bulkDelete'),
|
||||
group: 'bulk-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<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>
|
||||
<Trash2 className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: handleBulkDelete,
|
||||
},
|
||||
@@ -688,9 +671,7 @@ export function MailPage() {
|
||||
label: t('mail.bulkMove'),
|
||||
group: 'bulk-actions',
|
||||
icon: (
|
||||
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||
</svg>
|
||||
<ArrowRight className="w-3.5 h-3.5" strokeWidth={2} />
|
||||
),
|
||||
onClick: () => setShowMoveDropdown((v) => !v),
|
||||
},
|
||||
@@ -703,10 +684,7 @@ export function MailPage() {
|
||||
if (loadingAccounts) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-6 w-6 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -833,9 +811,7 @@ export function MailPage() {
|
||||
aria-label="Zurück zu Ordnern"
|
||||
data-testid="mobile-back-to-folders"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Ordner</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -872,9 +848,7 @@ export function MailPage() {
|
||||
aria-label="Zurück zur Liste"
|
||||
data-testid="mobile-back-to-list"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<ChevronLeft className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Zurück</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { RuleEditor } from '@/components/mail/RuleEditor';
|
||||
import { LabelManager } from '@/components/mail/LabelManager';
|
||||
import { VacationResponder } from '@/components/mail/VacationResponder';
|
||||
import { PgpSettings } from '@/components/mail/PgpSettings';
|
||||
import { Loader2, Pencil } from 'lucide-react';
|
||||
import {
|
||||
fetchAccounts,
|
||||
createAccount,
|
||||
@@ -291,10 +292,7 @@ export function MailSettingsPage() {
|
||||
|
||||
{loading ? (
|
||||
<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">
|
||||
<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>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
) : accounts.length === 0 ? (
|
||||
<p className="text-sm text-secondary-500">{t('mail.noAccounts')}</p>
|
||||
@@ -324,9 +322,7 @@ export function MailSettingsPage() {
|
||||
aria-label={t('common.edit')}
|
||||
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">
|
||||
<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>
|
||||
<Pencil className="w-3.5 h-3.5" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
||||
import { usePasswordResetRequest } from '@/api/hooks';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Check } from 'lucide-react';
|
||||
|
||||
const resetRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
@@ -43,9 +44,7 @@ export function PasswordResetRequestPage() {
|
||||
<div className="w-full max-w-md">
|
||||
<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">
|
||||
<svg className="w-6 h-6 text-success-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<Check className="w-6 h-6 text-success-600" strokeWidth={2} />
|
||||
</div>
|
||||
<p className="text-secondary-700 mb-6">{t('auth.resetRequestSuccess')}</p>
|
||||
<Link to="/login" className="text-sm text-primary-600 hover:text-primary-700">
|
||||
|
||||
Reference in New Issue
Block a user