diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx index 8ec0669..10d8001 100644 --- a/frontend/src/components/CommandPalette.jsx +++ b/frontend/src/components/CommandPalette.jsx @@ -15,6 +15,7 @@ import { /** * Command Palette - глобальный поиск по командам (Ctrl+K) + * Простой подход со встроенным backdrop (как в HistoryModal) */ function CommandPalette() { const [isOpen, setIsOpen] = useState(false) @@ -102,169 +103,86 @@ function CommandPalette() { if (!isOpen) return null return ( - <> - {/* Backdrop */} +
setIsOpen(false)} + >
setIsOpen(false)} - style={{ zIndex: 1050 }} - >
- - {/* Modal */} -
e.stopPropagation()} + role="document" > -
-
- {/* Search input */} -
-
- - - - { - setSearchTerm(e.target.value) - setSelectedIndex(0) - }} - /> -
+
+ {/* Search input */} +
+
+ + + + { + setSearchTerm(e.target.value) + setSelectedIndex(0) + }} + />
+
- {/* Commands list */} -
- {filteredCommands.length === 0 ? ( -
- Команды не найдены -
- ) : ( -
- {filteredCommands.map((cmd, index) => { - const Icon = cmd.icon - return ( - - ) - })} -
- )} -
- - {/* Footer with hints */} -
-
-
навигация
-
выбрать
-
Esc закрыть
+ {/* Commands list */} +
+ {filteredCommands.length === 0 ? ( +
+ +
Команды не найдены
+ ) : ( +
+ {filteredCommands.map((cmd, index) => { + const Icon = cmd.icon + return ( + + ) + })} +
+ )} +
+ + {/* Footer with hint */} +
+
+ + + навигация • Enter выбрать • ESC закрыть +
- +
) } -/** - * Кнопка для показа shortcuts - */ -export function KeyboardShortcutsButton({ className = '' }) { - const [showModal, setShowModal] = useState(false) - - const shortcuts = [ - { keys: ['Ctrl', 'K'], description: 'Открыть поиск команд' }, - { keys: ['Ctrl', 'N'], description: 'Добавить новую запись' }, - { keys: ['Ctrl', 'S'], description: 'Сохранить изменения' }, - { keys: ['Ctrl', 'F'], description: 'Поиск по таблице' }, - { keys: ['Esc'], description: 'Закрыть модалку / Отменить' }, - { keys: ['↑', '↓'], description: 'Навигация по списку' }, - { keys: ['Enter'], description: 'Подтвердить / Выбрать' }, - ] - - // Если className содержит nav-link, используем ссылку вместо кнопки - const isNavLink = className.includes('nav-link') - - return ( - <> - {isNavLink ? ( - { e.preventDefault(); setShowModal(true); }} - title="Горячие клавиши (Ctrl+/)" - > - - - ) : ( - - )} - - {showModal && ( - <> -
setShowModal(false)} - >
-
-
-
-
-
Горячие клавиши
- -
-
-
- {shortcuts.map((shortcut, index) => ( -
- {shortcut.description} -
- {shortcut.keys.map((key, i) => ( - {key} - ))} -
-
- ))} -
-
-
-
-
- - )} - - ) -} - -export default CommandPalette \ No newline at end of file +export default CommandPalette diff --git a/frontend/src/components/ConfirmDialog.jsx b/frontend/src/components/ConfirmDialog.jsx index a60b7bb..f46cf10 100644 --- a/frontend/src/components/ConfirmDialog.jsx +++ b/frontend/src/components/ConfirmDialog.jsx @@ -1,9 +1,8 @@ -import { SmallModal } from './Modal'; -import { IconAlertTriangle, IconCheck, IconX } from '@tabler/icons-react'; +import { IconAlertTriangle } from '@tabler/icons-react'; /** - * ConfirmDialog - упрощённая версия ConfirmModal для быстрых подтверждений - * Алиас для ConfirmModal с другим API (для обратной совместимости) + * ConfirmDialog - упрощённая версия для быстрых подтверждений + * Простой подход со встроенным backdrop (как в HistoryModal) */ export default function ConfirmDialog({ open, @@ -17,47 +16,78 @@ export default function ConfirmDialog({ size = 'sm', loading = false }) { + if (!open) return null; + return ( - - {destructive && ( - - )} - {title} -
- } - footer={ - <> - - - - } +
{ if (e.key === 'Escape') onCancel?.(); }} > -
-

{message}

+
+
{ + if (e.key === 'Tab') { + const c = e.currentTarget; + const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (!focusable || focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }} + > +
+
{title}
+
+ +
+
+ {destructive && ( +
+ +
+ )} +
+

{message}

+
+
+
+ +
+ + +
+
- +
); } diff --git a/frontend/src/components/ConfirmDiffModal.jsx b/frontend/src/components/ConfirmDiffModal.jsx index c17c8a9..9bee46d 100644 --- a/frontend/src/components/ConfirmDiffModal.jsx +++ b/frontend/src/components/ConfirmDiffModal.jsx @@ -1,101 +1,103 @@ -import { SmallModal } from './Modal'; import { IconCheck, IconX, IconRefresh } from '@tabler/icons-react'; /** * ConfirmDiffModal - модальное окно для подтверждения изменений с отображением статистики - * Использует готовый пресет SmallModal для компактного отображения + * Простой подход со встроенным backdrop (как в HistoryModal) */ function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) { const added = diff?.added?.length || 0; const removed = diff?.removed?.length || 0; const changed = diff?.changed?.length || 0; + if (!show) return null; + return ( - - - Подтвердить сохранение -
- } - footer={ - <> - - - - } +
{ if (e.key === 'Escape') onClose?.(); }} > -
-
-
-
-
- -
-
Добавлено
-
{added}
+
+
{ + if (e.key === 'Tab') { + const c = e.currentTarget; + const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (!focusable || focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }} + > +
+
Подтвердить сохранение
+
+ +
+
+
+
+
+ Добавлено +
{added}
+
+
+
+
+
+
+ Удалено +
{removed}
+
+
+
+
+
+
+ Изменено +
{changed}
+
-
-
-
-
-
- -
-
Удалено
-
{removed}
-
-
-
-
-
-
-
-
-
- -
-
Изменено
-
{changed}
-
-
-
+ +
+ +
- - {(added > 0 || removed > 0 || changed > 0) && ( -
-
- Всего будет изменено {added + removed + changed} записей -
-
- )} - +
); } diff --git a/frontend/src/components/ConfirmModal.jsx b/frontend/src/components/ConfirmModal.jsx index 188f17c..71f450c 100644 --- a/frontend/src/components/ConfirmModal.jsx +++ b/frontend/src/components/ConfirmModal.jsx @@ -1,9 +1,8 @@ -import { SmallModal } from './Modal'; -import { IconAlertTriangle, IconCheck, IconX } from '@tabler/icons-react'; +import { IconAlertTriangle } from '@tabler/icons-react'; /** * ConfirmModal - модальное окно подтверждения действия - * Использует SmallModal для компактного отображения + * Простой подход со встроенным backdrop (как в HistoryModal) */ function ConfirmModal({ show, @@ -13,65 +12,86 @@ function ConfirmModal({ message, confirmLabel = 'Подтвердить', cancelLabel = 'Отмена', - variant = 'danger', // primary, danger, warning, success + variant = 'danger', icon: Icon = IconAlertTriangle, loading = false, showIcon = true }) { + if (!show) return null; + const handleConfirm = () => { onConfirm?.(); }; - const getIconColor = () => { - switch (variant) { - case 'danger': return 'text-danger'; - case 'warning': return 'text-warning'; - case 'success': return 'text-success'; - case 'primary': return 'text-primary'; - default: return 'text-danger'; - } - }; - return ( - - {showIcon && Icon && ( - - )} - {title} -
- } - footer={ - <> - - - - } +
{ if (e.key === 'Escape') onClose?.(); }} > -
- {typeof message === 'string' ?

{message}

: message} +
+
{ + if (e.key === 'Tab') { + const c = e.currentTarget; + const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); + if (!focusable || focusable.length === 0) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }} + > +
+
{title}
+
+ +
+
+ {showIcon && Icon && ( +
+ +
+ )} +
+ {typeof message === 'string' ?

{message}

: message} +
+
+
+ +
+ + +
+
- +
); } diff --git a/frontend/src/components/FormModal.jsx b/frontend/src/components/FormModal.jsx index 858ce40..c4c5dc4 100644 --- a/frontend/src/components/FormModal.jsx +++ b/frontend/src/components/FormModal.jsx @@ -1,8 +1,6 @@ -import Modal from './Modal'; - /** * FormModal - модальное окно с формой - * Автоматически обрабатывает submit и отображает кнопки действий + * Простой подход со встроенным backdrop (как в HistoryModal) */ function FormModal({ show, @@ -17,8 +15,11 @@ function FormModal({ loading = false, submitVariant = 'primary', disabled = false, - ...modalProps + size = 'md', + centered = false }) { + if (!show) return null; + const handleSubmit = (e) => { e.preventDefault(); onSubmit?.(e); @@ -29,44 +30,62 @@ function FormModal({ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { handleSubmit(e); } + // ESC для закрытия + if (e.key === 'Escape') { + onClose?.(); + } }; return ( - - - - - } - {...modalProps} +
-
- {children} -
- +
+
+
+
+
{title}
+
+ +
+ {children} +
+ +
+ + +
+
+
+
+
); } export default FormModal; - diff --git a/frontend/src/components/ImportModal.jsx b/frontend/src/components/ImportModal.jsx index ffe3114..d8eec5f 100644 --- a/frontend/src/components/ImportModal.jsx +++ b/frontend/src/components/ImportModal.jsx @@ -1,10 +1,9 @@ -import Modal from './Modal'; import { useEffect, useRef, useState } from 'react'; import { IconUpload, IconFileText, IconAlertCircle, IconCheck, IconX } from '@tabler/icons-react'; /** * ImportModal - модальное окно импорта данных - * Рефакторинг: использует базовый Modal, улучшенный UX + * Простой подход со встроенным backdrop (как в HistoryModal) */ function ImportModal({ show, @@ -33,6 +32,8 @@ function ImportModal({ } }, [show]); + if (!show) return null; + const parseText = (raw) => { const lines = String(raw || '') .split(/\r?\n/) @@ -86,140 +87,148 @@ function ImportModal({ }; return ( - - - {title} -
- } - size="lg" - centered - footer={ - <> - - - - - } +
{ if (e.key === 'Escape') onClose?.(); }} > - {/* Description */} -
-
- -
{description}
-
-
- - {/* Drop zone */} -
{ e.preventDefault(); setDragOver(true); }} - onDragLeave={() => setDragOver(false)} - onDrop={handleDrop} - > -
-
-
- {fileName ? ( -
- - Файл: {fileName} -
- ) : ( - 'Перетащите TXT/CSV сюда или вставьте текст ниже' - )} +
+
+
+
+ + {title} +
+
+ +
+ {/* Description */} +
+
+ +
{description}
+
-