Phase 0.2: migrate remaining SVGs to lucide-react icons
This commit is contained in:
@@ -14,6 +14,7 @@ import { RichTextEditor } from './RichTextEditor';
|
||||
import type { Mail, MailSignature, SendMailPayload, ReplyPayload, ForwardPayload, MailDraftPayload } from '@/api/mail';
|
||||
import { uploadAttachment, type UploadedAttachment, replaceSignatureVariables } from '@/api/mail';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { FileText, Paperclip, X } from 'lucide-react';
|
||||
|
||||
export type ComposeMode = 'new' | 'reply' | 'forward' | 'draft';
|
||||
|
||||
@@ -309,9 +310,7 @@ export function ComposeModal({
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
isLoading={uploadingFile}
|
||||
icon={
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||
</svg>
|
||||
<Paperclip className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
}
|
||||
>
|
||||
{t('mail.addAttachment')}
|
||||
@@ -322,9 +321,7 @@ export function ComposeModal({
|
||||
<ul className="mt-2 space-y-1">
|
||||
{attachments.map((att) => (
|
||||
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md bg-secondary-50">
|
||||
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<FileText className="w-5 h-5 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-secondary-800 truncate">{att.filename}</p>
|
||||
<p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p>
|
||||
@@ -335,9 +332,7 @@ export function ComposeModal({
|
||||
className="p-1 rounded hover:bg-secondary-200 text-secondary-500"
|
||||
aria-label={t('common.remove')}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { Loader2, X } from 'lucide-react';
|
||||
import {
|
||||
fetchLabels,
|
||||
createLabel,
|
||||
@@ -81,10 +82,7 @@ export function LabelManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8" data-testid="label-manager-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -155,9 +153,7 @@ export function LabelManager() {
|
||||
data-testid={`delete-label-${label.id}`}
|
||||
type="button"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<X className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Mail, MailAttachment } from '@/api/mail';
|
||||
import { decodeMimeHeader } from '@/api/mail';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { FileText, Loader2 } from 'lucide-react';
|
||||
|
||||
export interface MailDetailProps {
|
||||
mail: Mail | null; loading: boolean;
|
||||
@@ -75,10 +76,7 @@ export function MailDetail({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12" data-testid="mail-detail-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -158,9 +156,7 @@ export function MailDetail({
|
||||
<ul className="space-y-1 md:space-y-2">
|
||||
{mail.attachments.map((att) => (
|
||||
<li key={att.id} className="flex items-center gap-3 p-2 rounded-md hover:bg-secondary-50 min-h-touch">
|
||||
<svg className="w-5 h-5 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<FileText className="w-5 h-5 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-secondary-800 truncate">{decodeMimeHeader(att.filename)}</p>
|
||||
<p className="text-xs text-secondary-400">{formatBytes(att.size_bytes)}</p>
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { MailAccount, MailFolder } from '@/api/mail';
|
||||
import { emptyFolder } from '@/api/mail';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { ChevronRight, Folder, Loader2 } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* Map IMAP folder names to German display names.
|
||||
@@ -88,23 +89,13 @@ function buildFolderTree(folders: MailFolder[], accountId: string): MailFolder[]
|
||||
|
||||
function ChevronIcon({ expanded }: { expanded: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
className={clsx('w-4 h-4 transition-transform flex-shrink-0', expanded && 'rotate-90')}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<ChevronRight className={clsx('w-4 h-4 transition-transform flex-shrink-0', expanded && 'rotate-90')} aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
|
||||
function FolderIcon() {
|
||||
return (
|
||||
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
||||
</svg>
|
||||
<Folder className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -304,10 +295,7 @@ export function MailFolderTree({ accounts, folders, selectedFolderId, onSelect,
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8" data-testid="folder-tree-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { Mail } from '@/api/mail';
|
||||
import { decodeMimeHeader } from '@/api/mail';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
import { ChevronUp, Loader2, Paperclip, Star } from 'lucide-react';
|
||||
|
||||
export interface MailListProps {
|
||||
mails: Mail[];
|
||||
@@ -70,10 +71,7 @@ export function MailList({
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12" data-testid="mail-list-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
@@ -132,11 +130,7 @@ export function MailList({
|
||||
title={sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}
|
||||
data-testid="mail-sort-order"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
{sortOrder === 'asc'
|
||||
? <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||
: <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />}
|
||||
</svg>
|
||||
<ChevronUp className="w-3 h-3" aria-hidden="true" strokeWidth={2} />
|
||||
<span>{sortOrder === 'asc' ? t('mail.sortAsc') : t('mail.sortDesc')}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -180,16 +174,12 @@ export function MailList({
|
||||
{mail.flag_type === 'important' && <span className="text-sm">❗</span>}
|
||||
{mail.flag_type === 'question' && <span className="text-sm">❓</span>}
|
||||
{(!mail.flag_type || mail.flag_type === '') && (
|
||||
<svg className="w-4 h-4 text-warning-500" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
<Star className="w-4 h-4 text-warning-500" fill="currentColor" />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{mail.has_attachments && (
|
||||
<svg className="w-4 h-4 text-secondary-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 13l-3.586 3.586a2 2 0 01-2.828 0L5 12.828a2 2 0 010-2.828l5.657-5.657a2 2 0 012.828 0L17 6.343M14.828 8.172a2 2 0 00-2.828 0l-3.586 3.586a2 2 0 000 2.828l1.414 1.414a2 2 0 002.828 0" />
|
||||
</svg>
|
||||
<Paperclip className="w-4 h-4 text-secondary-400 flex-shrink-0" aria-hidden="true" strokeWidth={2} />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
export interface MailSearchBarProps {
|
||||
onSearch: (query: string) => void;
|
||||
@@ -38,9 +39,7 @@ export function MailSearchBar({ onSearch }: MailSearchBarProps) {
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 rounded-md hover:bg-secondary-100 min-h-touch min-w-touch"
|
||||
aria-label={t('common.search')}
|
||||
>
|
||||
<svg className="w-4 h-4 text-secondary-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<Search className="w-4 h-4 text-secondary-400" aria-hidden="true" strokeWidth={2} />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Input } from '@/components/ui/Input';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
importPgpKey,
|
||||
fetchPgpKeys,
|
||||
@@ -83,10 +85,7 @@ export function PgpSettings() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8" data-testid="pgp-settings-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { TextStyle } from '@tiptap/extension-text-style';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import Placeholder from '@tiptap/extension-placeholder';
|
||||
import clsx from 'clsx';
|
||||
import { AlignCenter, AlignLeft, AlignRight, Bold, ChevronsLeft, Code, Heading, Image as ImageIcon, Italic, Link2, List, Menu, Quote, Redo2, Strikethrough, Underline as UnderlineIcon, Undo2 } from 'lucide-react';
|
||||
|
||||
export interface RichTextEditorProps {
|
||||
content: string;
|
||||
@@ -121,10 +122,10 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
|
||||
<div className="flex flex-wrap items-center gap-0.5 border-b border-secondary-200 bg-secondary-50 px-2 py-1" data-testid="rich-text-toolbar">
|
||||
{/* Undo / Redo */}
|
||||
<ToolbarButton onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()} label={t('common.undo')}>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" /></svg>
|
||||
<Undo2 className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()} label={t('common.redo')}>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 10H11a8 8 0 00-8 8v2m18-10l-6 6m6-6l-6-6" /></svg>
|
||||
<Redo2 className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
@@ -144,58 +145,58 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
|
||||
|
||||
{/* Inline formatting */}
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleBold().run()} active={editor.isActive('bold')} label={t('mail.bold')}>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6V4z M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6v-8z" /></svg>
|
||||
<Bold className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleItalic().run()} active={editor.isActive('italic')} label={t('mail.italic')}>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 4l-7 16M10 4L3 20M4 8h6M14 8h6" /></svg>
|
||||
<Italic className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleUnderline().run()} active={editor.isActive('underline')} label="Underline">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 3v8a6 6 0 006 6 6 6 0 006-6V3M4 21h16" /></svg>
|
||||
<UnderlineIcon className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleStrike().run()} active={editor.isActive('strike')} label="Strikethrough">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 12h16M9 5l6 14M15 5l-6 14" /></svg>
|
||||
<Strikethrough className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* Lists */}
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleBulletList().run()} active={editor.isActive('bulletList')} label="Bullet list">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16M8 6h.01M8 12h.01M8 18h.01" /></svg>
|
||||
<Menu className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleOrderedList().run()} active={editor.isActive('orderedList')} label="Numbered list">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 6h12M7 12h12M7 18h12M3 6h.01M3 12h.01M3 18h.01" /></svg>
|
||||
<List className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleBlockquote().run()} active={editor.isActive('blockquote')} label="Quote">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5h6v6H3zM3 13h6v6H3zM15 5h6v6h-6zM15 13h6v6h-6z" /></svg>
|
||||
<Quote className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleCode().run()} active={editor.isActive('code')} label="Inline code">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 7l-5 5 5 5M14 7l5 5-5 5" /></svg>
|
||||
<ChevronsLeft className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().toggleCodeBlock().run()} active={editor.isActive('codeBlock')} label="Code block">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l-3 3 3 3M16 9l3 3-3 3M13 5l-2 14" /></svg>
|
||||
<Code className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* Alignment */}
|
||||
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('left').run()} active={editor.isActive({ textAlign: 'left' })} label="Align left">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h10M4 18h12" /></svg>
|
||||
<AlignLeft className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('center').run()} active={editor.isActive({ textAlign: 'center' })} label="Align center">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M7 12h10M5 18h14" /></svg>
|
||||
<AlignCenter className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('right').run()} active={editor.isActive({ textAlign: 'right' })} label="Align right">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M10 12h10M8 18h12" /></svg>
|
||||
<AlignRight className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={() => editor.chain().focus().setTextAlign('justify').run()} active={editor.isActive({ textAlign: 'justify' })} label="Justify">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg>
|
||||
<Menu className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* Color */}
|
||||
<label className="p-1.5 rounded hover:bg-secondary-100 cursor-pointer" title="Text color">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 8h10M7 8l5 8 5-8M12 16v4" /></svg>
|
||||
<Heading className="w-4 h-4" strokeWidth={2} />
|
||||
<input
|
||||
type="color"
|
||||
className="sr-only"
|
||||
@@ -208,17 +209,17 @@ export function RichTextEditor({ content, onChange, placeholder, editable = true
|
||||
|
||||
{/* Link & Image */}
|
||||
<ToolbarButton onClick={setLink} active={editor.isActive('link')} label={t('mail.insertLink')}>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" /></svg>
|
||||
<Link2 className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={addImage} label="Insert image">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||
<ImageIcon className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
|
||||
<div className="w-px h-6 bg-secondary-200 mx-1" />
|
||||
|
||||
{/* Clear formatting */}
|
||||
<ToolbarButton onClick={() => editor.chain().focus().unsetAllMarks().run()} label="Clear formatting">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M9 7l3-1m0 0l3 9a5.002 5.002 0 006.001 0M15 6l-3 1" /></svg>
|
||||
<Quote className="w-4 h-4" strokeWidth={2} />
|
||||
</ToolbarButton>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { Card } from '@/components/ui/Card';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
fetchRules,
|
||||
createRule,
|
||||
@@ -132,10 +134,7 @@ export function RuleEditor({ accountId }: { accountId: string }) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8" data-testid="rule-editor-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { EmptyState } from '@/components/ui/EmptyState';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
|
||||
import { RichTextEditor } from './RichTextEditor';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
fetchSignatures,
|
||||
createSignature,
|
||||
@@ -112,10 +114,7 @@ export function SignatureManager() {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8" data-testid="signature-manager-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { fetchTemplates, substituteTemplate, type MailTemplate } from '@/api/mail';
|
||||
import { EmptyState } from '@/components/ui/EmptyState';
|
||||
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export interface TemplatePickerProps {
|
||||
onSelect: (body: string, subject: string) => void;
|
||||
}
|
||||
@@ -55,10 +57,7 @@ export function TemplatePicker({ onSelect }: TemplatePickerProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-4" data-testid="template-picker-loading">
|
||||
<svg className="animate-spin h-5 w-5 text-secondary-400" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<Loader2 className="animate-spin h-5 w-5 text-secondary-400" aria-hidden="true" />
|
||||
<span className="ml-2 text-sm text-secondary-500">{t('common.loading')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user