mirror of
https://github.com/yggdrasil-network/yggdrasil-go.git
synced 2025-08-24 16:05:07 +03:00
Implement modal system for adding peers and logout confirmation in WebUI
This commit is contained in:
parent
fcb5efd753
commit
19710fbc19
7 changed files with 789 additions and 32 deletions
|
@ -239,30 +239,61 @@ function createPeerElement(peer) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a new peer
|
||||
* Add a new peer with modal form
|
||||
*/
|
||||
async function addPeer() {
|
||||
const uri = prompt('Enter peer URI:\nExamples:\n• tcp://example.com:54321\n• tls://peer.yggdrasil.network:443');
|
||||
if (!uri || uri.trim() === '') {
|
||||
showModal({
|
||||
title: 'add_peer',
|
||||
content: 'add_peer_modal_description',
|
||||
size: 'medium',
|
||||
inputs: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'peer_uri',
|
||||
label: 'peer_uri_label',
|
||||
placeholder: 'peer_uri_placeholder',
|
||||
required: true,
|
||||
help: 'peer_uri_help'
|
||||
}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
text: 'modal_cancel',
|
||||
type: 'secondary',
|
||||
action: 'close'
|
||||
},
|
||||
{
|
||||
text: 'add_peer_btn',
|
||||
type: 'primary',
|
||||
callback: async (formData) => {
|
||||
const uri = formData.peer_uri?.trim();
|
||||
|
||||
if (!uri) {
|
||||
showWarning('Peer URI is required');
|
||||
return;
|
||||
return false; // Don't close modal
|
||||
}
|
||||
|
||||
// Basic URI validation
|
||||
if (!uri.includes('://')) {
|
||||
showError('Invalid URI format. Must include protocol (tcp://, tls://, etc.)');
|
||||
return;
|
||||
return false; // Don't close modal
|
||||
}
|
||||
|
||||
try {
|
||||
showInfo('Adding peer...');
|
||||
await window.yggAPI.addPeer(uri.trim());
|
||||
showSuccess(`Peer added successfully: ${uri.trim()}`);
|
||||
await window.yggAPI.addPeer(uri);
|
||||
showSuccess(`Peer added successfully: ${uri}`);
|
||||
await loadPeers(); // Refresh peer list
|
||||
return true; // Close modal
|
||||
} catch (error) {
|
||||
showError('Failed to add peer: ' + error.message);
|
||||
return false; // Don't close modal
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove peer with confirmation
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
<script src="static/api.js"></script>
|
||||
<script src="static/main.js"></script>
|
||||
<script src="static/app.js"></script>
|
||||
<script src="static/modal.js"></script>
|
||||
<script src="static/lang/ru.js"></script>
|
||||
<script src="static/lang/en.js"></script>
|
||||
</head>
|
||||
|
@ -146,6 +147,21 @@
|
|||
<!-- Notifications container -->
|
||||
<div class="notifications-container" id="notifications-container"></div>
|
||||
|
||||
<!-- Modal System -->
|
||||
<div class="modal-overlay" id="modal-overlay">
|
||||
<div class="modal-container" id="modal-container">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title" id="modal-title">Modal Title</h3>
|
||||
<button class="modal-close-btn" id="modal-close-btn" onclick="closeModal()">✕</button>
|
||||
</div>
|
||||
<div class="modal-content" id="modal-content">
|
||||
<!-- Modal content will be injected here -->
|
||||
</div>
|
||||
<div class="modal-footer" id="modal-footer">
|
||||
<!-- Modal buttons will be injected here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
|
|
|
@ -78,5 +78,25 @@ window.translations.en = {
|
|||
'peer_quality_good': 'Good',
|
||||
'peer_quality_fair': 'Fair',
|
||||
'peer_quality_poor': 'Poor',
|
||||
'peer_quality_unknown': 'Unknown'
|
||||
'peer_quality_unknown': 'Unknown',
|
||||
|
||||
// Modal translations
|
||||
'modal_close': 'Close',
|
||||
'modal_cancel': 'Cancel',
|
||||
'modal_ok': 'OK',
|
||||
'modal_confirm': 'Confirmation',
|
||||
'modal_confirm_yes': 'Yes',
|
||||
'modal_confirm_message': 'Are you sure?',
|
||||
'modal_alert': 'Alert',
|
||||
'modal_input': 'Input',
|
||||
'modal_error': 'Error',
|
||||
'modal_success': 'Success',
|
||||
'modal_warning': 'Warning',
|
||||
'modal_info': 'Information',
|
||||
|
||||
// Add peer modal
|
||||
'add_peer_modal_description': 'Enter peer URI to connect to a new network node',
|
||||
'peer_uri_label': 'Peer URI',
|
||||
'peer_uri_placeholder': 'tcp://example.com:54321',
|
||||
'peer_uri_help': 'Examples: tcp://example.com:54321, tls://peer.yggdrasil.network:443'
|
||||
};
|
|
@ -78,5 +78,25 @@ window.translations.ru = {
|
|||
'peer_quality_good': 'Хорошее',
|
||||
'peer_quality_fair': 'Приемлемое',
|
||||
'peer_quality_poor': 'Плохое',
|
||||
'peer_quality_unknown': 'Неизвестно'
|
||||
'peer_quality_unknown': 'Неизвестно',
|
||||
|
||||
// Modal translations
|
||||
'modal_close': 'Закрыть',
|
||||
'modal_cancel': 'Отмена',
|
||||
'modal_ok': 'ОК',
|
||||
'modal_confirm': 'Подтверждение',
|
||||
'modal_confirm_yes': 'Да',
|
||||
'modal_confirm_message': 'Вы уверены?',
|
||||
'modal_alert': 'Уведомление',
|
||||
'modal_input': 'Ввод данных',
|
||||
'modal_error': 'Ошибка',
|
||||
'modal_success': 'Успешно',
|
||||
'modal_warning': 'Предупреждение',
|
||||
'modal_info': 'Информация',
|
||||
|
||||
// Add peer modal
|
||||
'add_peer_modal_description': 'Введите URI пира для подключения к новому узлу сети',
|
||||
'peer_uri_label': 'URI пира',
|
||||
'peer_uri_placeholder': 'tcp://example.com:54321',
|
||||
'peer_uri_help': 'Примеры: tcp://example.com:54321, tls://peer.yggdrasil.network:443'
|
||||
};
|
|
@ -170,10 +170,16 @@ function showSection(sectionName) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Logout function (placeholder)
|
||||
* Logout function with modal confirmation
|
||||
*/
|
||||
function logout() {
|
||||
if (confirm('Are you sure you want to logout?')) {
|
||||
showConfirmModal({
|
||||
title: 'modal_confirm',
|
||||
message: 'logout_confirm',
|
||||
confirmText: 'modal_confirm_yes',
|
||||
cancelText: 'modal_cancel',
|
||||
type: 'danger',
|
||||
onConfirm: () => {
|
||||
// Clear stored preferences
|
||||
localStorage.removeItem('yggdrasil-language');
|
||||
localStorage.removeItem('yggdrasil-theme');
|
||||
|
@ -181,6 +187,7 @@ function logout() {
|
|||
// Redirect or refresh
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notification system
|
||||
|
|
415
src/webui/static/modal.js
Normal file
415
src/webui/static/modal.js
Normal file
|
@ -0,0 +1,415 @@
|
|||
/**
|
||||
* Modal System for Yggdrasil Web Interface
|
||||
* Provides flexible modal dialogs with multiple action buttons and input forms
|
||||
*/
|
||||
|
||||
// Global modal state
|
||||
let currentModal = null;
|
||||
let modalCallbacks = {};
|
||||
|
||||
/**
|
||||
* Show a modal dialog
|
||||
* @param {Object} options - Modal configuration
|
||||
* @param {string} options.title - Modal title (supports localization key)
|
||||
* @param {string|HTMLElement} options.content - Modal content (supports localization key)
|
||||
* @param {Array} options.buttons - Array of button configurations
|
||||
* @param {Array} options.inputs - Array of input field configurations
|
||||
* @param {Function} options.onClose - Callback when modal is closed
|
||||
* @param {boolean} options.closable - Whether modal can be closed by clicking overlay or X button
|
||||
* @param {string} options.size - Modal size: 'small', 'medium', 'large'
|
||||
*/
|
||||
function showModal(options = {}) {
|
||||
const {
|
||||
title = 'Modal',
|
||||
content = '',
|
||||
buttons = [{ text: 'modal_close', type: 'secondary', action: 'close' }],
|
||||
inputs = [],
|
||||
onClose = null,
|
||||
closable = true,
|
||||
size = 'medium'
|
||||
} = options;
|
||||
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
const container = document.getElementById('modal-container');
|
||||
const titleElement = document.getElementById('modal-title');
|
||||
const contentElement = document.getElementById('modal-content');
|
||||
const footerElement = document.getElementById('modal-footer');
|
||||
const closeBtn = document.getElementById('modal-close-btn');
|
||||
|
||||
if (!overlay || !container) {
|
||||
console.error('Modal elements not found in DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set modal size
|
||||
container.className = `modal-container modal-${size}`;
|
||||
|
||||
// Set title (with localization support)
|
||||
titleElement.textContent = getLocalizedText(title);
|
||||
|
||||
// Set content
|
||||
if (typeof content === 'string') {
|
||||
contentElement.innerHTML = `<p>${getLocalizedText(content)}</p>`;
|
||||
} else if (content instanceof HTMLElement) {
|
||||
contentElement.innerHTML = '';
|
||||
contentElement.appendChild(content);
|
||||
} else {
|
||||
contentElement.innerHTML = content;
|
||||
}
|
||||
|
||||
// Add input fields if provided
|
||||
if (inputs && inputs.length > 0) {
|
||||
const formContainer = document.createElement('div');
|
||||
formContainer.className = 'modal-form-container';
|
||||
|
||||
inputs.forEach((input, index) => {
|
||||
const formGroup = createFormGroup(input, index);
|
||||
formContainer.appendChild(formGroup);
|
||||
});
|
||||
|
||||
contentElement.appendChild(formContainer);
|
||||
}
|
||||
|
||||
// Create buttons
|
||||
footerElement.innerHTML = '';
|
||||
buttons.forEach((button, index) => {
|
||||
const btn = createModalButton(button, index);
|
||||
footerElement.appendChild(btn);
|
||||
});
|
||||
|
||||
// Configure close button
|
||||
closeBtn.style.display = closable ? 'flex' : 'none';
|
||||
|
||||
// Set up event handlers
|
||||
modalCallbacks.onClose = onClose;
|
||||
|
||||
// Close on overlay click (if closable)
|
||||
if (closable) {
|
||||
overlay.onclick = (e) => {
|
||||
if (e.target === overlay) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
overlay.onclick = null;
|
||||
}
|
||||
|
||||
// Close on Escape key (if closable)
|
||||
if (closable) {
|
||||
document.addEventListener('keydown', handleEscapeKey);
|
||||
}
|
||||
|
||||
// Show modal
|
||||
currentModal = options;
|
||||
overlay.classList.add('show');
|
||||
|
||||
// Focus first input if available
|
||||
setTimeout(() => {
|
||||
const firstInput = contentElement.querySelector('.modal-form-input');
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current modal
|
||||
*/
|
||||
function closeModal() {
|
||||
const overlay = document.getElementById('modal-overlay');
|
||||
if (!overlay) return;
|
||||
|
||||
overlay.classList.remove('show');
|
||||
|
||||
// Clean up event listeners
|
||||
document.removeEventListener('keydown', handleEscapeKey);
|
||||
overlay.onclick = null;
|
||||
|
||||
// Call onClose callback if provided
|
||||
if (modalCallbacks.onClose) {
|
||||
modalCallbacks.onClose();
|
||||
}
|
||||
|
||||
// Clear state
|
||||
currentModal = null;
|
||||
modalCallbacks = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a form group element
|
||||
*/
|
||||
function createFormGroup(input, index) {
|
||||
const {
|
||||
type = 'text',
|
||||
name = `input_${index}`,
|
||||
label = '',
|
||||
placeholder = '',
|
||||
value = '',
|
||||
required = false,
|
||||
help = '',
|
||||
options = [] // for select inputs
|
||||
} = input;
|
||||
|
||||
const formGroup = document.createElement('div');
|
||||
formGroup.className = 'modal-form-group';
|
||||
|
||||
// Create label
|
||||
if (label) {
|
||||
const labelElement = document.createElement('label');
|
||||
labelElement.className = 'modal-form-label';
|
||||
labelElement.textContent = getLocalizedText(label);
|
||||
labelElement.setAttribute('for', name);
|
||||
formGroup.appendChild(labelElement);
|
||||
}
|
||||
|
||||
// Create input element
|
||||
let inputElement;
|
||||
|
||||
if (type === 'textarea') {
|
||||
inputElement = document.createElement('textarea');
|
||||
inputElement.className = 'modal-form-textarea';
|
||||
} else if (type === 'select') {
|
||||
inputElement = document.createElement('select');
|
||||
inputElement.className = 'modal-form-select';
|
||||
|
||||
// Add options
|
||||
options.forEach(option => {
|
||||
const optionElement = document.createElement('option');
|
||||
optionElement.value = option.value || option;
|
||||
optionElement.textContent = getLocalizedText(option.text || option);
|
||||
if (option.selected || option.value === value) {
|
||||
optionElement.selected = true;
|
||||
}
|
||||
inputElement.appendChild(optionElement);
|
||||
});
|
||||
} else {
|
||||
inputElement = document.createElement('input');
|
||||
inputElement.type = type;
|
||||
inputElement.className = 'modal-form-input';
|
||||
inputElement.value = value;
|
||||
}
|
||||
|
||||
inputElement.name = name;
|
||||
inputElement.id = name;
|
||||
|
||||
if (placeholder) {
|
||||
inputElement.placeholder = getLocalizedText(placeholder);
|
||||
}
|
||||
|
||||
if (required) {
|
||||
inputElement.required = true;
|
||||
}
|
||||
|
||||
formGroup.appendChild(inputElement);
|
||||
|
||||
// Create help text
|
||||
if (help) {
|
||||
const helpElement = document.createElement('div');
|
||||
helpElement.className = 'modal-form-help';
|
||||
helpElement.textContent = getLocalizedText(help);
|
||||
formGroup.appendChild(helpElement);
|
||||
}
|
||||
|
||||
return formGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a modal button
|
||||
*/
|
||||
function createModalButton(button, index) {
|
||||
const {
|
||||
text = 'Button',
|
||||
type = 'secondary',
|
||||
action = null,
|
||||
callback = null,
|
||||
disabled = false
|
||||
} = button;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `modal-btn modal-btn-${type}`;
|
||||
btn.textContent = getLocalizedText(text);
|
||||
btn.disabled = disabled;
|
||||
|
||||
btn.onclick = () => {
|
||||
if (action === 'close') {
|
||||
closeModal();
|
||||
} else if (callback) {
|
||||
const formData = getModalFormData();
|
||||
const result = callback(formData);
|
||||
|
||||
// If callback returns false, don't close modal
|
||||
if (result !== false) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form data from modal inputs
|
||||
*/
|
||||
function getModalFormData() {
|
||||
const formData = {};
|
||||
const inputs = document.querySelectorAll('#modal-content .modal-form-input, #modal-content .modal-form-textarea, #modal-content .modal-form-select');
|
||||
|
||||
inputs.forEach(input => {
|
||||
formData[input.name] = input.value;
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Escape key press
|
||||
*/
|
||||
function handleEscapeKey(e) {
|
||||
if (e.key === 'Escape' && currentModal) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized text or return original if not found
|
||||
*/
|
||||
function getLocalizedText(key) {
|
||||
if (typeof key !== 'string') return key;
|
||||
|
||||
const currentLang = window.getCurrentLanguage ? window.getCurrentLanguage() : 'en';
|
||||
|
||||
if (window.translations &&
|
||||
window.translations[currentLang] &&
|
||||
window.translations[currentLang][key]) {
|
||||
return window.translations[currentLang][key];
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
// Convenience functions for common modal types
|
||||
|
||||
/**
|
||||
* Show a confirmation dialog
|
||||
*/
|
||||
function showConfirmModal(options = {}) {
|
||||
const {
|
||||
title = 'modal_confirm',
|
||||
message = 'modal_confirm_message',
|
||||
confirmText = 'modal_confirm_yes',
|
||||
cancelText = 'modal_cancel',
|
||||
onConfirm = null,
|
||||
onCancel = null,
|
||||
type = 'danger' // danger, primary, success
|
||||
} = options;
|
||||
|
||||
showModal({
|
||||
title,
|
||||
content: message,
|
||||
closable: true,
|
||||
buttons: [
|
||||
{
|
||||
text: cancelText,
|
||||
type: 'secondary',
|
||||
action: 'close',
|
||||
callback: onCancel
|
||||
},
|
||||
{
|
||||
text: confirmText,
|
||||
type: type,
|
||||
callback: () => {
|
||||
if (onConfirm) onConfirm();
|
||||
return true; // Close modal
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show an alert dialog
|
||||
*/
|
||||
function showAlertModal(options = {}) {
|
||||
const {
|
||||
title = 'modal_alert',
|
||||
message = '',
|
||||
buttonText = 'modal_ok',
|
||||
type = 'primary',
|
||||
onClose = null
|
||||
} = options;
|
||||
|
||||
showModal({
|
||||
title,
|
||||
content: message,
|
||||
closable: true,
|
||||
onClose,
|
||||
buttons: [
|
||||
{
|
||||
text: buttonText,
|
||||
type: type,
|
||||
action: 'close'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a prompt dialog with input
|
||||
*/
|
||||
function showPromptModal(options = {}) {
|
||||
const {
|
||||
title = 'modal_input',
|
||||
message = '',
|
||||
inputLabel = '',
|
||||
inputPlaceholder = '',
|
||||
inputValue = '',
|
||||
inputType = 'text',
|
||||
inputRequired = true,
|
||||
confirmText = 'modal_ok',
|
||||
cancelText = 'modal_cancel',
|
||||
onConfirm = null,
|
||||
onCancel = null
|
||||
} = options;
|
||||
|
||||
showModal({
|
||||
title,
|
||||
content: message,
|
||||
closable: true,
|
||||
inputs: [
|
||||
{
|
||||
type: inputType,
|
||||
name: 'input_value',
|
||||
label: inputLabel,
|
||||
placeholder: inputPlaceholder,
|
||||
value: inputValue,
|
||||
required: inputRequired
|
||||
}
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
text: cancelText,
|
||||
type: 'secondary',
|
||||
action: 'close',
|
||||
callback: onCancel
|
||||
},
|
||||
{
|
||||
text: confirmText,
|
||||
type: 'primary',
|
||||
callback: (formData) => {
|
||||
if (inputRequired && !formData.input_value.trim()) {
|
||||
return false; // Don't close modal
|
||||
}
|
||||
if (onConfirm) onConfirm(formData.input_value);
|
||||
return true; // Close modal
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// Export functions to global scope
|
||||
window.showModal = showModal;
|
||||
window.closeModal = closeModal;
|
||||
window.showConfirmModal = showConfirmModal;
|
||||
window.showAlertModal = showAlertModal;
|
||||
window.showPromptModal = showPromptModal;
|
|
@ -1316,3 +1316,251 @@ button[onclick="copyNodeKey()"]:hover {
|
|||
/* Responsive design for peer items */
|
||||
@media (max-width: 768px) {
|
||||
}
|
||||
|
||||
/* ======================== */
|
||||
/* MODAL SYSTEM */
|
||||
/* ======================== */
|
||||
|
||||
/* Modal overlay background */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Modal container */
|
||||
.modal-container {
|
||||
background: var(--bg-info-card);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px var(--shadow-heavy);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
transform: scale(0.9) translateY(-20px);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--border-card);
|
||||
}
|
||||
|
||||
/* Modal sizes */
|
||||
.modal-small {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.modal-medium {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.modal-large {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.modal-overlay.show .modal-container {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
/* Modal header */
|
||||
.modal-header {
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid var(--border-card);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-nav-item);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
color: var(--text-heading);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: var(--bg-nav-hover);
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
/* Modal content */
|
||||
.modal-content {
|
||||
padding: 24px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
color: var(--text-body);
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-content p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Modal footer */
|
||||
.modal-footer {
|
||||
padding: 16px 24px 20px;
|
||||
border-top: 1px solid var(--border-card);
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
background: var(--bg-nav-item);
|
||||
}
|
||||
|
||||
/* Modal buttons */
|
||||
.modal-btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.modal-btn-primary {
|
||||
background: var(--bg-nav-active);
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
.modal-btn-primary:hover {
|
||||
background: var(--bg-nav-active-border);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-btn-secondary {
|
||||
background: var(--bg-nav-item);
|
||||
color: var(--text-nav);
|
||||
border: 1px solid var(--border-nav-item);
|
||||
}
|
||||
|
||||
.modal-btn-secondary:hover {
|
||||
background: var(--bg-nav-hover);
|
||||
}
|
||||
|
||||
.modal-btn-danger {
|
||||
background: var(--bg-logout);
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
.modal-btn-danger:hover {
|
||||
background: var(--bg-logout-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-btn-success {
|
||||
background: var(--bg-success-dark);
|
||||
color: var(--text-white);
|
||||
}
|
||||
|
||||
.modal-btn-success:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Modal form elements */
|
||||
.modal-form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-form-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-heading);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-form-input,
|
||||
.modal-form-textarea,
|
||||
.modal-form-select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-nav-item);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
background: var(--bg-info-card);
|
||||
color: var(--text-body);
|
||||
transition: border-color 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-form-input:focus,
|
||||
.modal-form-textarea:focus,
|
||||
.modal-form-select:focus {
|
||||
outline: none;
|
||||
border-color: var(--bg-nav-active);
|
||||
}
|
||||
|
||||
.modal-form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.modal-form-help {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
.modal-container {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.modal-header,
|
||||
.modal-content,
|
||||
.modal-footer {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.modal-btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue