Add EntitySelect/IconSelect UI improvements across modals
- Portal IconSelect popups to document.body with position:fixed to prevent clipping by modal overflow-y:auto - Replace custom scene selectors in automation editor with EntitySelect command-palette pickers (main scene + fallback scene) - Add IconSelect grid for automation deactivation mode (none/revert/fallback) - Add IconSelect grid for automation condition type and match type - Replace mapped zone source dropdowns with EntitySelect pickers - Replace scene target selector with EntityPalette.pick() pattern - Remove effect palette preview bar from CSS editor - Remove sensitivity badge from audio color strip source cards - Clean up unused scene-selector CSS and scene-target-add-row CSS - Add locale keys for all new UI elements across en/ru/zh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICO
|
||||
import * as P from '../core/icon-paths.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
import { IconSelect } from '../core/icon-select.js';
|
||||
import { EntitySelect } from '../core/entity-palette.js';
|
||||
import { attachProcessPicker } from '../core/process-picker.js';
|
||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||
|
||||
@@ -227,6 +228,7 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
condList.innerHTML = '';
|
||||
|
||||
_ensureConditionLogicIconSelect();
|
||||
_ensureDeactivationModeIconSelect();
|
||||
|
||||
// Fetch scenes for selector
|
||||
try {
|
||||
@@ -235,6 +237,7 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
|
||||
// Reset deactivation mode
|
||||
document.getElementById('automation-deactivation-mode').value = 'none';
|
||||
if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue('none');
|
||||
document.getElementById('automation-fallback-scene-group').style.display = 'none';
|
||||
|
||||
if (automationId) {
|
||||
@@ -255,12 +258,14 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
}
|
||||
|
||||
// Scene selector
|
||||
_initSceneSelector('automation-scene', automation.scene_preset_id);
|
||||
_initSceneSelector('automation-scene-id', automation.scene_preset_id);
|
||||
|
||||
// Deactivation mode
|
||||
document.getElementById('automation-deactivation-mode').value = automation.deactivation_mode || 'none';
|
||||
const deactMode = automation.deactivation_mode || 'none';
|
||||
document.getElementById('automation-deactivation-mode').value = deactMode;
|
||||
if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue(deactMode);
|
||||
_onDeactivationModeChange();
|
||||
_initSceneSelector('automation-fallback-scene', automation.deactivation_scene_preset_id);
|
||||
_initSceneSelector('automation-fallback-scene-id', automation.deactivation_scene_preset_id);
|
||||
} catch (e) {
|
||||
showToast(e.message, 'error');
|
||||
return;
|
||||
@@ -281,11 +286,13 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
addAutomationConditionRow(clonedCond);
|
||||
}
|
||||
|
||||
_initSceneSelector('automation-scene', cloneData.scene_preset_id);
|
||||
_initSceneSelector('automation-scene-id', cloneData.scene_preset_id);
|
||||
|
||||
document.getElementById('automation-deactivation-mode').value = cloneData.deactivation_mode || 'none';
|
||||
const cloneDeactMode = cloneData.deactivation_mode || 'none';
|
||||
document.getElementById('automation-deactivation-mode').value = cloneDeactMode;
|
||||
if (_deactivationModeIconSelect) _deactivationModeIconSelect.setValue(cloneDeactMode);
|
||||
_onDeactivationModeChange();
|
||||
_initSceneSelector('automation-fallback-scene', cloneData.deactivation_scene_preset_id);
|
||||
_initSceneSelector('automation-fallback-scene-id', cloneData.deactivation_scene_preset_id);
|
||||
} else {
|
||||
titleEl.innerHTML = `${ICON_AUTOMATION} ${t('automations.add')}`;
|
||||
idInput.value = '';
|
||||
@@ -293,8 +300,8 @@ export async function openAutomationEditor(automationId, cloneData) {
|
||||
enabledInput.checked = true;
|
||||
logicSelect.value = 'or';
|
||||
if (_conditionLogicIconSelect) _conditionLogicIconSelect.setValue('or');
|
||||
_initSceneSelector('automation-scene', null);
|
||||
_initSceneSelector('automation-fallback-scene', null);
|
||||
_initSceneSelector('automation-scene-id', null);
|
||||
_initSceneSelector('automation-fallback-scene-id', null);
|
||||
}
|
||||
|
||||
// Wire up deactivation mode change
|
||||
@@ -319,102 +326,60 @@ export async function closeAutomationEditorModal() {
|
||||
await automationModal.close();
|
||||
}
|
||||
|
||||
// ===== Scene selector logic =====
|
||||
// ===== Scene selector (EntitySelect) =====
|
||||
|
||||
function _initSceneSelector(prefix, selectedId) {
|
||||
const hiddenInput = document.getElementById(`${prefix}-id`);
|
||||
const searchInput = document.getElementById(`${prefix}-search`);
|
||||
const clearBtn = document.getElementById(`${prefix}-clear`);
|
||||
const dropdown = document.getElementById(`${prefix}-dropdown`);
|
||||
let _sceneEntitySelect = null;
|
||||
let _fallbackSceneEntitySelect = null;
|
||||
|
||||
hiddenInput.value = selectedId || '';
|
||||
function _getSceneItems() {
|
||||
return (scenePresetsCache.data || []).map(s => ({
|
||||
value: s.id,
|
||||
label: s.name,
|
||||
icon: `<span class="scene-color-dot" style="background:${s.color || '#4fc3f7'}"></span>`,
|
||||
}));
|
||||
}
|
||||
|
||||
// Set initial display text
|
||||
if (selectedId) {
|
||||
const scene = scenePresetsCache.data.find(s => s.id === selectedId);
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
} else {
|
||||
searchInput.value = '';
|
||||
clearBtn.classList.toggle('visible', false);
|
||||
}
|
||||
function _initSceneSelector(selectId, selectedId) {
|
||||
const sel = document.getElementById(selectId);
|
||||
// Populate <select> with scene options
|
||||
sel.innerHTML = (scenePresetsCache.data || []).map(s =>
|
||||
`<option value="${s.id}">${escapeHtml(s.name)}</option>`
|
||||
).join('');
|
||||
sel.value = selectedId || '';
|
||||
|
||||
// Render dropdown items
|
||||
function renderDropdown(filter) {
|
||||
const query = (filter || '').toLowerCase();
|
||||
const filtered = query ? scenePresetsCache.data.filter(s => s.name.toLowerCase().includes(query)) : scenePresetsCache.data;
|
||||
// Determine which EntitySelect slot to use
|
||||
const isMain = selectId === 'automation-scene-id';
|
||||
const existing = isMain ? _sceneEntitySelect : _fallbackSceneEntitySelect;
|
||||
if (existing) existing.destroy();
|
||||
|
||||
if (filtered.length === 0) {
|
||||
dropdown.innerHTML = `<div class="scene-selector-empty">${t('automations.scene.none_available')}</div>`;
|
||||
} else {
|
||||
dropdown.innerHTML = filtered.map(s => {
|
||||
const selected = s.id === hiddenInput.value ? ' selected' : '';
|
||||
return `<div class="scene-selector-item${selected}" data-scene-id="${s.id}"><span class="scene-color-dot" style="background:${escapeHtml(s.color || '#4fc3f7')}"></span>${escapeHtml(s.name)}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
const es = new EntitySelect({
|
||||
target: sel,
|
||||
getItems: _getSceneItems,
|
||||
placeholder: t('automations.scene.search_placeholder'),
|
||||
allowNone: true,
|
||||
noneLabel: t('automations.scene.none_selected'),
|
||||
});
|
||||
if (isMain) _sceneEntitySelect = es; else _fallbackSceneEntitySelect = es;
|
||||
}
|
||||
|
||||
// Attach click handlers
|
||||
dropdown.querySelectorAll('.scene-selector-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.sceneId;
|
||||
const scene = scenePresetsCache.data.find(s => s.id === id);
|
||||
hiddenInput.value = id;
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
dropdown.classList.remove('open');
|
||||
});
|
||||
});
|
||||
}
|
||||
// ===== Deactivation mode IconSelect =====
|
||||
|
||||
// Show dropdown on focus/click
|
||||
searchInput.onfocus = () => {
|
||||
renderDropdown(searchInput.value);
|
||||
dropdown.classList.add('open');
|
||||
};
|
||||
const DEACT_MODE_KEYS = ['none', 'revert', 'fallback_scene'];
|
||||
const DEACT_MODE_ICONS = {
|
||||
none: P.pause, revert: P.undo2, fallback_scene: P.sparkles,
|
||||
};
|
||||
let _deactivationModeIconSelect = null;
|
||||
|
||||
searchInput.oninput = () => {
|
||||
renderDropdown(searchInput.value);
|
||||
dropdown.classList.add('open');
|
||||
// If text doesn't match any scene, clear the hidden input
|
||||
const exactMatch = scenePresetsCache.data.find(s => s.name.toLowerCase() === searchInput.value.toLowerCase());
|
||||
if (!exactMatch) {
|
||||
hiddenInput.value = '';
|
||||
clearBtn.classList.toggle('visible', !!searchInput.value);
|
||||
}
|
||||
};
|
||||
|
||||
searchInput.onkeydown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
// Select first visible item
|
||||
const first = dropdown.querySelector('.scene-selector-item');
|
||||
if (first) first.click();
|
||||
} else if (e.key === 'Escape') {
|
||||
dropdown.classList.remove('open');
|
||||
searchInput.blur();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear button
|
||||
clearBtn.onclick = () => {
|
||||
hiddenInput.value = '';
|
||||
searchInput.value = '';
|
||||
clearBtn.classList.remove('visible');
|
||||
dropdown.classList.remove('open');
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const selectorEl = searchInput.closest('.scene-selector');
|
||||
// Remove old listener if any (re-init)
|
||||
if (selectorEl._outsideClickHandler) {
|
||||
document.removeEventListener('click', selectorEl._outsideClickHandler);
|
||||
}
|
||||
selectorEl._outsideClickHandler = (e) => {
|
||||
if (!selectorEl.contains(e.target)) {
|
||||
dropdown.classList.remove('open');
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', selectorEl._outsideClickHandler);
|
||||
function _ensureDeactivationModeIconSelect() {
|
||||
const sel = document.getElementById('automation-deactivation-mode');
|
||||
if (!sel || _deactivationModeIconSelect) return;
|
||||
const items = DEACT_MODE_KEYS.map(k => ({
|
||||
value: k,
|
||||
icon: _icon(DEACT_MODE_ICONS[k]),
|
||||
label: t(`automations.deactivation_mode.${k}`),
|
||||
desc: t(`automations.deactivation_mode.${k}.desc`),
|
||||
}));
|
||||
_deactivationModeIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
||||
}
|
||||
|
||||
// ===== Condition editor =====
|
||||
@@ -423,6 +388,36 @@ export function addAutomationCondition() {
|
||||
addAutomationConditionRow({ condition_type: 'application', apps: [], match_type: 'running' });
|
||||
}
|
||||
|
||||
const CONDITION_TYPE_KEYS = ['always', 'startup', 'application', 'time_of_day', 'system_idle', 'display_state', 'mqtt', 'webhook'];
|
||||
const CONDITION_TYPE_ICONS = {
|
||||
always: P.refreshCw, startup: P.power, application: P.smartphone,
|
||||
time_of_day: P.clock, system_idle: P.moon, display_state: P.monitor,
|
||||
mqtt: P.radio, webhook: P.globe,
|
||||
};
|
||||
|
||||
const MATCH_TYPE_KEYS = ['running', 'topmost', 'topmost_fullscreen', 'fullscreen'];
|
||||
const MATCH_TYPE_ICONS = {
|
||||
running: P.play, topmost: P.monitor, topmost_fullscreen: P.tv, fullscreen: P.square,
|
||||
};
|
||||
|
||||
function _buildMatchTypeItems() {
|
||||
return MATCH_TYPE_KEYS.map(k => ({
|
||||
value: k,
|
||||
icon: _icon(MATCH_TYPE_ICONS[k]),
|
||||
label: t(`automations.condition.application.match_type.${k}`),
|
||||
desc: t(`automations.condition.application.match_type.${k}.desc`),
|
||||
}));
|
||||
}
|
||||
|
||||
function _buildConditionTypeItems() {
|
||||
return CONDITION_TYPE_KEYS.map(k => ({
|
||||
value: k,
|
||||
icon: _icon(CONDITION_TYPE_ICONS[k]),
|
||||
label: t(`automations.condition.${k}`),
|
||||
desc: t(`automations.condition.${k}.desc`),
|
||||
}));
|
||||
}
|
||||
|
||||
function addAutomationConditionRow(condition) {
|
||||
const list = document.getElementById('automation-conditions-list');
|
||||
const row = document.createElement('div');
|
||||
@@ -432,14 +427,7 @@ function addAutomationConditionRow(condition) {
|
||||
row.innerHTML = `
|
||||
<div class="condition-header">
|
||||
<select class="condition-type-select">
|
||||
<option value="always" ${condType === 'always' ? 'selected' : ''}>${t('automations.condition.always')}</option>
|
||||
<option value="startup" ${condType === 'startup' ? 'selected' : ''}>${t('automations.condition.startup')}</option>
|
||||
<option value="application" ${condType === 'application' ? 'selected' : ''}>${t('automations.condition.application')}</option>
|
||||
<option value="time_of_day" ${condType === 'time_of_day' ? 'selected' : ''}>${t('automations.condition.time_of_day')}</option>
|
||||
<option value="system_idle" ${condType === 'system_idle' ? 'selected' : ''}>${t('automations.condition.system_idle')}</option>
|
||||
<option value="display_state" ${condType === 'display_state' ? 'selected' : ''}>${t('automations.condition.display_state')}</option>
|
||||
<option value="mqtt" ${condType === 'mqtt' ? 'selected' : ''}>${t('automations.condition.mqtt')}</option>
|
||||
<option value="webhook" ${condType === 'webhook' ? 'selected' : ''}>${t('automations.condition.webhook')}</option>
|
||||
${CONDITION_TYPE_KEYS.map(k => `<option value="${k}" ${condType === k ? 'selected' : ''}>${t('automations.condition.' + k)}</option>`).join('')}
|
||||
</select>
|
||||
<button type="button" class="btn-remove-condition" onclick="this.closest('.automation-condition-row').remove()" title="Remove">✕</button>
|
||||
</div>
|
||||
@@ -449,6 +437,13 @@ function addAutomationConditionRow(condition) {
|
||||
const typeSelect = row.querySelector('.condition-type-select');
|
||||
const container = row.querySelector('.condition-fields-container');
|
||||
|
||||
// Attach IconSelect to the condition type dropdown
|
||||
const condIconSelect = new IconSelect({
|
||||
target: typeSelect,
|
||||
items: _buildConditionTypeItems(),
|
||||
columns: 4,
|
||||
});
|
||||
|
||||
function renderFields(type, data) {
|
||||
if (type === 'always') {
|
||||
container.innerHTML = `<small class="condition-always-desc">${t('automations.condition.always.hint')}</small>`;
|
||||
@@ -585,6 +580,16 @@ function addAutomationConditionRow(condition) {
|
||||
`;
|
||||
const textarea = container.querySelector('.condition-apps');
|
||||
attachProcessPicker(container, textarea);
|
||||
|
||||
// Attach IconSelect to match type
|
||||
const matchSel = container.querySelector('.condition-match-type');
|
||||
if (matchSel) {
|
||||
new IconSelect({
|
||||
target: matchSel,
|
||||
items: _buildMatchTypeItems(),
|
||||
columns: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderFields(condType, condition);
|
||||
|
||||
Reference in New Issue
Block a user