feat: Рефакторинг модальных окон с заменой на простой подход со встроенным backdrop для улучшения UX. Обновление компонентов CommandPalette, ConfirmDialog, ConfirmDiffModal, ConfirmModal, FormModal, ImportModal, SettingsModal и WsUpdateModal для повышения удобства использования и унификации интерфейса.
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m9s

This commit is contained in:
2025-10-03 12:52:39 +07:00
parent 9bff239035
commit f87d8f90c3
8 changed files with 659 additions and 641 deletions
+72 -154
View File
@@ -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 */}
<div
className="modal show d-block"
role="dialog"
aria-modal="true"
style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}
onClick={() => setIsOpen(false)}
>
<div
className="modal-backdrop fade show"
onClick={() => setIsOpen(false)}
style={{ zIndex: 1050 }}
></div>
{/* Modal */}
<div
className="modal fade show d-block"
tabIndex="-1"
style={{ zIndex: 1055 }}
className="modal-dialog modal-dialog-centered"
style={{ maxWidth: '600px' }}
onClick={(e) => e.stopPropagation()}
role="document"
>
<div className="modal-dialog modal-dialog-centered" style={{ maxWidth: '600px' }}>
<div className="modal-content">
{/* Search input */}
<div className="modal-header border-0 pb-0">
<div className="input-icon w-100">
<span className="input-icon-addon">
<IconSearch size={20} />
</span>
<input
ref={inputRef}
type="text"
className="form-control form-control-lg border-0"
placeholder="Поиск команд..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value)
setSelectedIndex(0)
}}
/>
</div>
<div className="modal-content" tabIndex={-1}>
{/* Search input */}
<div className="modal-header border-0 pb-0">
<div className="input-icon w-100">
<span className="input-icon-addon">
<IconSearch size={18} />
</span>
<input
ref={inputRef}
type="text"
className="form-control form-control-lg border-0"
placeholder="Поиск команд..."
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value)
setSelectedIndex(0)
}}
/>
</div>
</div>
{/* Commands list */}
<div className="modal-body pt-2" style={{ maxHeight: '400px', overflowY: 'auto' }}>
{filteredCommands.length === 0 ? (
<div className="text-center text-muted py-4">
Команды не найдены
</div>
) : (
<div className="list-group list-group-flush">
{filteredCommands.map((cmd, index) => {
const Icon = cmd.icon
return (
<button
key={index}
className={`list-group-item list-group-item-action d-flex align-items-center ${selectedIndex === index ? 'active' : ''}`}
onClick={() => executeCommand(cmd)}
onMouseEnter={() => setSelectedIndex(index)}
>
<span className={`avatar avatar-sm me-3 ${selectedIndex === index ? 'bg-white text-primary' : 'bg-blue-lt text-blue'}`}>
<Icon size={20} />
</span>
<div className="flex-grow-1 text-start">
<div className="fw-bold">{cmd.label}</div>
<div className="text-muted small">{cmd.description}</div>
</div>
{selectedIndex === index && (
<kbd className="bg-white text-muted border"></kbd>
)}
</button>
)
})}
</div>
)}
</div>
{/* Footer with hints */}
<div className="modal-footer border-0 pt-0">
<div className="d-flex gap-3 text-muted small w-100 justify-content-center">
<div><kbd></kbd> <kbd></kbd> навигация</div>
<div><kbd></kbd> выбрать</div>
<div><kbd>Esc</kbd> закрыть</div>
{/* Commands list */}
<div className="modal-body pt-2">
{filteredCommands.length === 0 ? (
<div className="text-center text-muted py-4">
<IconSearch size={48} className="mb-2 opacity-50" />
<div>Команды не найдены</div>
</div>
) : (
<div className="list-group list-group-flush">
{filteredCommands.map((cmd, index) => {
const Icon = cmd.icon
return (
<button
key={index}
type="button"
className={`list-group-item list-group-item-action d-flex align-items-center${
index === selectedIndex ? ' active' : ''
}`}
onClick={() => executeCommand(cmd)}
onMouseEnter={() => setSelectedIndex(index)}
>
<Icon size={20} className="me-3" />
<div className="flex-grow-1 text-start">
<div className="fw-bold">{cmd.label}</div>
<small className="text-muted">{cmd.description}</small>
</div>
</button>
)
})}
</div>
)}
</div>
{/* Footer with hint */}
<div className="modal-footer border-0 pt-0">
<div className="text-muted small d-flex align-items-center gap-2">
<IconKeyboard size={16} />
<span>
<kbd></kbd> <kbd></kbd> навигация <kbd>Enter</kbd> выбрать <kbd>ESC</kbd> закрыть
</span>
</div>
</div>
</div>
</div>
</>
</div>
)
}
/**
* Кнопка для показа 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 ? (
<a
href="#"
className={className}
onClick={(e) => { e.preventDefault(); setShowModal(true); }}
title="Горячие клавиши (Ctrl+/)"
>
<IconKeyboard size={20} />
</a>
) : (
<button
className={`btn btn-ghost-secondary btn-icon ${className}`}
onClick={() => setShowModal(true)}
title="Горячие клавиши (Ctrl+/)"
>
<IconKeyboard size={20} />
</button>
)}
{showModal && (
<>
<div
className="modal-backdrop fade show"
onClick={() => setShowModal(false)}
></div>
<div className="modal fade show d-block" tabIndex="-1">
<div className="modal-dialog modal-dialog-centered">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Горячие клавиши</h5>
<button
type="button"
className="btn-close"
onClick={() => setShowModal(false)}
></button>
</div>
<div className="modal-body">
<div className="list-group list-group-flush">
{shortcuts.map((shortcut, index) => (
<div key={index} className="list-group-item d-flex justify-content-between align-items-center">
<span>{shortcut.description}</span>
<div className="d-flex gap-1">
{shortcut.keys.map((key, i) => (
<kbd key={i} className="bg-secondary-lt">{key}</kbd>
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</>
)}
</>
)
}
export default CommandPalette
export default CommandPalette