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