Add automation group management — rename, delete, bulk toggle, drag-drop
Full automation page upgrade with group management and drag-and-drop: Backend: batch_update_group() and bulk_set_enabled() DB methods, new PUT /api/automations/group and POST /api/automations/bulk-toggle endpoints. Group headers: rename (inline edit), delete (choice dialog — keep automations or delete all), bulk toggle (enable/disable all in group). Actions appear on hover, styled as small icon buttons. Drag and drop: non-system cards are draggable between group sections. Drop zones show dashed accent border feedback. Collapsed sections auto-expand on 500ms drag-hover. System/Hub sections dimmed during drag. dragenter counter pattern handles child element bubbling. Delete group dialog: glass card modal with three options — keep automations (move to My Automations), delete everything, or cancel.
This commit is contained in:
parent
7317bd7c55
commit
85b470809e
4 changed files with 538 additions and 4 deletions
|
|
@ -10794,6 +10794,44 @@ class MusicDatabase:
|
|||
logger.error(f"Error deleting automation {automation_id}: {e}")
|
||||
return False
|
||||
|
||||
def batch_update_group(self, automation_ids: list, group_name: str = None) -> int:
|
||||
"""Batch update group_name for multiple automations. Excludes system automations."""
|
||||
if not automation_ids:
|
||||
return 0
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
placeholders = ','.join('?' for _ in automation_ids)
|
||||
cursor.execute(
|
||||
f"UPDATE automations SET group_name = ?, updated_at = CURRENT_TIMESTAMP "
|
||||
f"WHERE id IN ({placeholders}) AND (is_system IS NULL OR is_system = 0)",
|
||||
[group_name] + list(automation_ids)
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
except Exception as e:
|
||||
logger.error(f"Error batch updating group: {e}")
|
||||
return 0
|
||||
|
||||
def bulk_set_enabled(self, automation_ids: list, enabled: bool) -> int:
|
||||
"""Bulk enable/disable multiple automations. Excludes system automations."""
|
||||
if not automation_ids:
|
||||
return 0
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
placeholders = ','.join('?' for _ in automation_ids)
|
||||
cursor.execute(
|
||||
f"UPDATE automations SET enabled = ?, updated_at = CURRENT_TIMESTAMP "
|
||||
f"WHERE id IN ({placeholders}) AND (is_system IS NULL OR is_system = 0)",
|
||||
[1 if enabled else 0] + list(automation_ids)
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount
|
||||
except Exception as e:
|
||||
logger.error(f"Error bulk toggling automations: {e}")
|
||||
return 0
|
||||
|
||||
def toggle_automation(self, automation_id: int) -> bool:
|
||||
"""Toggle the enabled state of an automation. Returns True on success."""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -6103,6 +6103,67 @@ def update_automation_endpoint(automation_id):
|
|||
logger.error(f"Error updating automation: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route('/api/automations/group', methods=['PUT'])
|
||||
def batch_update_automation_group():
|
||||
"""Batch update group_name for multiple automations (rename group, delete group, drag-drop)."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
automation_ids = data.get('automation_ids', [])
|
||||
group_name = data.get('group_name') # None/null = ungroup
|
||||
|
||||
if not automation_ids or not isinstance(automation_ids, list):
|
||||
return jsonify({"error": "automation_ids must be a non-empty list"}), 400
|
||||
|
||||
# Sanitize IDs to integers
|
||||
try:
|
||||
automation_ids = [int(aid) for aid in automation_ids]
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "automation_ids must contain integers"}), 400
|
||||
|
||||
db = get_database()
|
||||
updated = db.batch_update_group(automation_ids, group_name)
|
||||
|
||||
return jsonify({"success": True, "updated": updated})
|
||||
except Exception as e:
|
||||
logger.error(f"Error batch updating automation group: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/automations/bulk-toggle', methods=['POST'])
|
||||
def bulk_toggle_automations():
|
||||
"""Bulk enable/disable multiple automations."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
automation_ids = data.get('automation_ids', [])
|
||||
enabled = data.get('enabled', True)
|
||||
|
||||
if not automation_ids or not isinstance(automation_ids, list):
|
||||
return jsonify({"error": "automation_ids must be a non-empty list"}), 400
|
||||
|
||||
try:
|
||||
automation_ids = [int(aid) for aid in automation_ids]
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({"error": "automation_ids must contain integers"}), 400
|
||||
|
||||
db = get_database()
|
||||
updated = db.bulk_set_enabled(automation_ids, bool(enabled))
|
||||
|
||||
# Reschedule/cancel affected automations
|
||||
if automation_engine and updated > 0:
|
||||
for aid in automation_ids:
|
||||
auto = db.get_automation(aid)
|
||||
if auto:
|
||||
if auto.get('enabled'):
|
||||
automation_engine.schedule_automation(auto)
|
||||
else:
|
||||
automation_engine.cancel_automation(aid)
|
||||
|
||||
return jsonify({"success": True, "updated": updated})
|
||||
except Exception as e:
|
||||
logger.error(f"Error bulk toggling automations: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/automations/<int:automation_id>', methods=['DELETE'])
|
||||
def delete_automation_endpoint(automation_id):
|
||||
"""Delete an automation. System automations cannot be deleted."""
|
||||
|
|
|
|||
|
|
@ -68895,26 +68895,113 @@ const AUTO_HUB_REFERENCE = {
|
|||
|
||||
// --- Load & Render List ---
|
||||
|
||||
function _buildAutomationSection(id, label, automations, useGrid) {
|
||||
// Drag-and-drop state
|
||||
let _autoDragState = null;
|
||||
let _autoDragEnterCount = 0;
|
||||
let _autoDragExpandTimer = null;
|
||||
|
||||
function _buildAutomationSection(id, label, automations, useGrid, options = {}) {
|
||||
const groupName = options.groupName || null;
|
||||
const isProtected = options.isProtected || false; // System, Hub sections
|
||||
|
||||
const section = document.createElement('div');
|
||||
section.className = 'automations-section';
|
||||
if (isProtected) section.classList.add('section-protected');
|
||||
section.id = id;
|
||||
if (groupName) section.dataset.groupName = groupName;
|
||||
const collapsed = localStorage.getItem('auto_section_' + id) === '1';
|
||||
if (collapsed) section.classList.add('collapsed');
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'automations-section-header';
|
||||
|
||||
// Group header actions (rename, bulk toggle, delete) — only for user groups
|
||||
let actionsHtml = '';
|
||||
if (groupName && !isProtected) {
|
||||
const enabledCount = automations.filter(a => a.enabled).length;
|
||||
const allEnabled = enabledCount === automations.length;
|
||||
actionsHtml = `
|
||||
<div class="section-actions" onclick="event.stopPropagation();">
|
||||
<button class="section-action-btn" title="${allEnabled ? 'Disable all' : 'Enable all'}" onclick="_bulkToggleGroup('${_escAttr(groupName)}', ${allEnabled})">
|
||||
${allEnabled ? '⏸' : '▶'}
|
||||
</button>
|
||||
<button class="section-action-btn" title="Rename group" onclick="_startRenameGroup('${_escAttr(groupName)}', this)">
|
||||
✏️
|
||||
</button>
|
||||
<button class="section-action-btn section-action-danger" title="Delete group" onclick="_deleteGroup('${_escAttr(groupName)}')">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
header.innerHTML = `
|
||||
<span class="section-chevron">▼</span>
|
||||
<span class="section-label">${label}</span>
|
||||
<span class="section-count">${automations.length}</span>
|
||||
${actionsHtml}
|
||||
<span class="section-line"></span>
|
||||
`;
|
||||
header.onclick = () => {
|
||||
header.onclick = (e) => {
|
||||
if (e.target.closest('.section-actions')) return;
|
||||
section.classList.toggle('collapsed');
|
||||
localStorage.setItem('auto_section_' + id, section.classList.contains('collapsed') ? '1' : '0');
|
||||
};
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'automations-section-body';
|
||||
|
||||
// Drop zone setup (not for protected sections)
|
||||
if (!isProtected) {
|
||||
const dropGroupName = groupName; // null for "My Automations"
|
||||
body.addEventListener('dragover', (e) => {
|
||||
if (!_autoDragState) return;
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
body.classList.add('drop-target');
|
||||
});
|
||||
body.addEventListener('dragenter', (e) => {
|
||||
if (!_autoDragState) return;
|
||||
_autoDragEnterCount++;
|
||||
body.classList.add('drop-target');
|
||||
// Expand collapsed sections on drag-hover
|
||||
if (section.classList.contains('collapsed')) {
|
||||
_autoDragExpandTimer = setTimeout(() => {
|
||||
section.classList.remove('collapsed');
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
body.addEventListener('dragleave', (e) => {
|
||||
if (!_autoDragState) return;
|
||||
_autoDragEnterCount--;
|
||||
if (_autoDragEnterCount <= 0) {
|
||||
_autoDragEnterCount = 0;
|
||||
body.classList.remove('drop-target');
|
||||
if (_autoDragExpandTimer) { clearTimeout(_autoDragExpandTimer); _autoDragExpandTimer = null; }
|
||||
}
|
||||
});
|
||||
body.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
body.classList.remove('drop-target');
|
||||
_autoDragEnterCount = 0;
|
||||
if (!_autoDragState) return;
|
||||
const draggedId = _autoDragState.id;
|
||||
const fromGroup = _autoDragState.groupName;
|
||||
if (fromGroup === dropGroupName) return; // Same group, no-op
|
||||
try {
|
||||
const res = await fetch('/api/automations/' + draggedId, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ group_name: dropGroupName })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
showToast(dropGroupName ? `Moved to "${dropGroupName}"` : 'Moved to My Automations', 'success');
|
||||
await loadAutomations();
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
});
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = useGrid ? 'automations-grid' : 'automations-user-list';
|
||||
automations.forEach(a => container.appendChild(renderAutomationCard(a)));
|
||||
|
|
@ -68924,6 +69011,167 @@ function _buildAutomationSection(id, label, automations, useGrid) {
|
|||
return section;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a group — ungroups all automations (moves to My Automations).
|
||||
*/
|
||||
async function _deleteGroup(groupName) {
|
||||
// Collect automation IDs in this group
|
||||
const ids = [];
|
||||
document.querySelectorAll(`.automations-section[data-group-name="${groupName}"] .automation-card`).forEach(card => {
|
||||
if (card.dataset.id) ids.push(parseInt(card.dataset.id));
|
||||
});
|
||||
|
||||
if (ids.length === 0) { await loadAutomations(); return; }
|
||||
|
||||
// Show choice dialog — ungroup or delete all
|
||||
const choice = await _showDeleteGroupDialog(groupName, ids.length);
|
||||
if (!choice) return;
|
||||
|
||||
try {
|
||||
if (choice === 'ungroup') {
|
||||
const res = await fetch('/api/automations/group', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ automation_ids: ids, group_name: null })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
showToast(`Dissolved group "${groupName}" — ${data.updated} automations moved to My Automations`, 'success');
|
||||
} else if (choice === 'delete_all') {
|
||||
// Delete each automation
|
||||
let deleted = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
const res = await fetch('/api/automations/' + id, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.success) deleted++;
|
||||
} catch (e) {}
|
||||
}
|
||||
showToast(`Deleted group "${groupName}" and ${deleted} automation${deleted !== 1 ? 's' : ''}`, 'success');
|
||||
}
|
||||
await loadAutomations();
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
function _showDeleteGroupDialog(groupName, count) {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); resolve(null); } };
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="delete-group-dialog">
|
||||
<div class="delete-group-icon">🗑️</div>
|
||||
<h3 class="delete-group-title">Delete Group "${groupName}"</h3>
|
||||
<p class="delete-group-message">This group contains ${count} automation${count !== 1 ? 's' : ''}. What would you like to do?</p>
|
||||
<div class="delete-group-actions">
|
||||
<button class="delete-group-btn delete-group-keep" id="dg-ungroup">
|
||||
Keep Automations — move to My Automations
|
||||
</button>
|
||||
<button class="delete-group-btn delete-group-remove" id="dg-delete">
|
||||
Delete Everything — remove group and all ${count} automation${count !== 1 ? 's' : ''}
|
||||
</button>
|
||||
<button class="delete-group-btn delete-group-cancel" id="dg-cancel">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.querySelector('#dg-ungroup').onclick = () => { overlay.remove(); resolve('ungroup'); };
|
||||
overlay.querySelector('#dg-delete').onclick = () => { overlay.remove(); resolve('delete_all'); };
|
||||
overlay.querySelector('#dg-cancel').onclick = () => { overlay.remove(); resolve(null); };
|
||||
|
||||
document.addEventListener('keydown', function esc(e) {
|
||||
if (e.key === 'Escape') { overlay.remove(); resolve(null); document.removeEventListener('keydown', esc); }
|
||||
});
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a group — inline edit on the section header label.
|
||||
*/
|
||||
function _startRenameGroup(groupName, btnEl) {
|
||||
const section = btnEl.closest('.automations-section');
|
||||
const labelEl = section?.querySelector('.section-label');
|
||||
if (!labelEl) return;
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = 'section-rename-input';
|
||||
input.value = groupName;
|
||||
input.onclick = (e) => e.stopPropagation();
|
||||
|
||||
const originalText = labelEl.textContent;
|
||||
labelEl.textContent = '';
|
||||
labelEl.appendChild(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
|
||||
const finish = async (save) => {
|
||||
const newName = input.value.trim();
|
||||
input.removeEventListener('blur', blurHandler);
|
||||
if (!save || !newName || newName === groupName) {
|
||||
labelEl.textContent = originalText;
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = [];
|
||||
section.querySelectorAll('.automation-card').forEach(card => {
|
||||
if (card.dataset.id) ids.push(parseInt(card.dataset.id));
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/automations/group', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ automation_ids: ids, group_name: newName })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
showToast(`Renamed to "${newName}"`, 'success');
|
||||
await loadAutomations();
|
||||
} catch (err) {
|
||||
showToast('Error: ' + err.message, 'error');
|
||||
labelEl.textContent = originalText;
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') { e.preventDefault(); finish(true); }
|
||||
if (e.key === 'Escape') { finish(false); }
|
||||
});
|
||||
const blurHandler = () => finish(true);
|
||||
input.addEventListener('blur', blurHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk toggle all automations in a group.
|
||||
*/
|
||||
async function _bulkToggleGroup(groupName, currentlyAllEnabled) {
|
||||
const ids = [];
|
||||
document.querySelectorAll(`.automations-section[data-group-name="${groupName}"] .automation-card`).forEach(card => {
|
||||
if (card.dataset.id) ids.push(parseInt(card.dataset.id));
|
||||
});
|
||||
if (ids.length === 0) return;
|
||||
|
||||
const targetEnabled = !currentlyAllEnabled;
|
||||
try {
|
||||
const res = await fetch('/api/automations/bulk-toggle', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ automation_ids: ids, enabled: targetEnabled })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
showToast(`${targetEnabled ? 'Enabled' : 'Disabled'} ${data.updated} automations`, 'success');
|
||||
await loadAutomations();
|
||||
} catch (err) { showToast('Error: ' + err.message, 'error'); }
|
||||
}
|
||||
|
||||
async function loadAutomations() {
|
||||
const list = document.getElementById('automations-list');
|
||||
const empty = document.getElementById('automations-empty');
|
||||
|
|
@ -68945,7 +69193,7 @@ async function loadAutomations() {
|
|||
const userAutos = automations.filter(a => !a.is_system);
|
||||
|
||||
if (systemAutos.length) {
|
||||
list.appendChild(_buildAutomationSection('auto-section-system', 'System', systemAutos, true));
|
||||
list.appendChild(_buildAutomationSection('auto-section-system', 'System', systemAutos, true, { isProtected: true }));
|
||||
}
|
||||
|
||||
// Automation Hub section
|
||||
|
|
@ -68957,7 +69205,7 @@ async function loadAutomations() {
|
|||
groups.forEach(g => {
|
||||
const groupAutos = userAutos.filter(a => a.group_name === g);
|
||||
if (groupAutos.length) {
|
||||
list.appendChild(_buildAutomationSection('auto-section-group-' + g.replace(/\W+/g, '_'), '\uD83D\uDCC1 ' + g, groupAutos, true));
|
||||
list.appendChild(_buildAutomationSection('auto-section-group-' + g.replace(/\W+/g, '_'), '\uD83D\uDCC1 ' + g, groupAutos, true, { groupName: g }));
|
||||
}
|
||||
});
|
||||
if (ungrouped.length) {
|
||||
|
|
@ -69705,6 +69953,27 @@ function renderAutomationCard(a) {
|
|||
card.dataset.id = a.id;
|
||||
card.dataset.triggerType = a.trigger_type || '';
|
||||
card.dataset.actionType = a.action_type || '';
|
||||
|
||||
// Drag-and-drop (non-system only)
|
||||
if (!a.is_system) {
|
||||
card.draggable = true;
|
||||
card.addEventListener('dragstart', (e) => {
|
||||
_autoDragState = { id: a.id, groupName: a.group_name || null };
|
||||
e.dataTransfer.setData('text/plain', String(a.id));
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
card.classList.add('dragging');
|
||||
// Dim protected sections during drag
|
||||
document.querySelectorAll('.section-protected').forEach(s => s.classList.add('no-drop'));
|
||||
});
|
||||
card.addEventListener('dragend', () => {
|
||||
card.classList.remove('dragging');
|
||||
_autoDragState = null;
|
||||
_autoDragEnterCount = 0;
|
||||
document.querySelectorAll('.drop-target').forEach(el => el.classList.remove('drop-target'));
|
||||
document.querySelectorAll('.no-drop').forEach(el => el.classList.remove('no-drop'));
|
||||
if (_autoDragExpandTimer) { clearTimeout(_autoDragExpandTimer); _autoDragExpandTimer = null; }
|
||||
});
|
||||
}
|
||||
const tIcon = _autoIcons[a.trigger_type] || '\u2699\uFE0F';
|
||||
const aIcon = _autoIcons[a.action_type] || '\u2699\uFE0F';
|
||||
const tl = tIcon + ' ' + _autoFormatTrigger(a.trigger_type, a.trigger_config);
|
||||
|
|
|
|||
|
|
@ -41129,6 +41129,172 @@ body.downloads-disabled [onclick*="DownloadMissing"]:not([onclick*="close"]) {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
/* --- Group header actions --- */
|
||||
.section-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.automations-section-header:hover .section-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.section-action-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.section-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.section-action-danger:hover {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.section-rename-input {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(var(--accent-rgb), 0.4);
|
||||
border-radius: 4px;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 2px 8px;
|
||||
width: 180px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.section-rename-input:focus {
|
||||
border-color: rgba(var(--accent-rgb), 0.6);
|
||||
box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.15);
|
||||
}
|
||||
|
||||
/* --- Drag and drop --- */
|
||||
.automation-card[draggable="true"] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.automation-card[draggable="true"]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.automation-card.dragging {
|
||||
opacity: 0.35;
|
||||
transform: scale(0.97);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.automations-section-body.drop-target {
|
||||
background: rgba(var(--accent-rgb), 0.04);
|
||||
border: 2px dashed rgba(var(--accent-rgb), 0.25);
|
||||
border-radius: 10px;
|
||||
min-height: 50px;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.section-protected.no-drop {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- Delete group dialog --- */
|
||||
.delete-group-dialog {
|
||||
background: linear-gradient(135deg, rgba(26, 26, 26, 0.98) 0%, rgba(16, 16, 16, 0.99) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 28px 24px;
|
||||
max-width: 420px;
|
||||
width: 90vw;
|
||||
text-align: center;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.delete-group-icon {
|
||||
font-size: 32px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.delete-group-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.delete-group-message {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin: 0 0 18px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.delete-group-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-group-btn {
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.delete-group-keep {
|
||||
background: rgba(var(--accent-rgb), 0.12);
|
||||
color: rgb(var(--accent-light-rgb));
|
||||
border-color: rgba(var(--accent-rgb), 0.25);
|
||||
}
|
||||
|
||||
.delete-group-keep:hover {
|
||||
background: rgba(var(--accent-rgb), 0.2);
|
||||
border-color: rgba(var(--accent-rgb), 0.4);
|
||||
}
|
||||
|
||||
.delete-group-remove {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: #f87171;
|
||||
border-color: rgba(239, 68, 68, 0.25);
|
||||
}
|
||||
|
||||
.delete-group-remove:hover {
|
||||
background: rgba(239, 68, 68, 0.2);
|
||||
border-color: rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.delete-group-cancel {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.delete-group-cancel:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* --- System Automations 2-Column Grid --- */
|
||||
.automations-grid {
|
||||
display: grid;
|
||||
|
|
|
|||
Loading…
Reference in a new issue