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:
2026-03-09 16:00:30 +03:00
parent 186940124c
commit 2712c6682e
32 changed files with 1204 additions and 391 deletions
@@ -13,6 +13,7 @@ import {
} from '../core/icons.js';
import { scenePresetsCache } from '../core/state.js';
import { cardColorStyle, cardColorButton } from '../core/card-colors.js';
import { EntityPalette } from '../core/entity-palette.js';
let _editingId = null;
let _allTargets = []; // fetched on capture open
@@ -153,13 +154,37 @@ export async function editScenePreset(presetId) {
document.getElementById('scene-preset-editor-description').value = preset.description || '';
document.getElementById('scene-preset-editor-error').style.display = 'none';
// Hide target selector in edit mode (metadata only)
const selectorGroup = document.getElementById('scene-target-selector-group');
if (selectorGroup) selectorGroup.style.display = 'none';
const titleEl = document.querySelector('#scene-preset-editor-title span[data-i18n]');
if (titleEl) { titleEl.setAttribute('data-i18n', 'scenes.edit'); titleEl.textContent = t('scenes.edit'); }
// Show target selector and pre-populate with existing targets
const selectorGroup = document.getElementById('scene-target-selector-group');
const targetList = document.getElementById('scene-target-list');
if (selectorGroup && targetList) {
selectorGroup.style.display = '';
targetList.innerHTML = '';
try {
const resp = await fetchWithAuth('/output-targets');
if (resp.ok) {
const data = await resp.json();
_allTargets = data.targets || [];
// Pre-add targets already in the preset
const presetTargetIds = (preset.targets || []).map(pt => pt.target_id || pt.id);
for (const tid of presetTargetIds) {
const tgt = _allTargets.find(t => t.id === tid);
if (!tgt) continue;
const item = document.createElement('div');
item.className = 'scene-target-item';
item.dataset.targetId = tid;
item.innerHTML = `<span>${escapeHtml(tgt.name)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">&#x2715;</button>`;
targetList.appendChild(item);
}
_refreshTargetSelect();
}
} catch { /* ignore */ }
}
scenePresetModal.open();
scenePresetModal.snapshot();
}
@@ -180,9 +205,11 @@ export async function saveScenePreset() {
try {
let resp;
if (_editingId) {
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
.map(el => el.dataset.targetId);
resp = await fetchWithAuth(`/scene-presets/${_editingId}`, {
method: 'PUT',
body: JSON.stringify({ name, description }),
body: JSON.stringify({ name, description, target_ids }),
});
} else {
const target_ids = [...document.querySelectorAll('#scene-target-list .scene-target-item')]
@@ -216,42 +243,54 @@ export async function closeScenePresetEditor() {
// ===== Target selector helpers =====
function _refreshTargetSelect() {
const select = document.getElementById('scene-target-select');
if (!select) return;
const added = new Set(
function _getAddedTargetIds() {
return new Set(
[...document.querySelectorAll('#scene-target-list .scene-target-item')]
.map(el => el.dataset.targetId)
);
select.innerHTML = '';
for (const tgt of _allTargets) {
if (added.has(tgt.id)) continue;
const opt = document.createElement('option');
opt.value = tgt.id;
opt.textContent = tgt.name;
select.appendChild(opt);
}
// Disable add button when no targets available
const addBtn = select.parentElement?.querySelector('button');
if (addBtn) addBtn.disabled = select.options.length === 0;
}
export function addSceneTarget() {
const select = document.getElementById('scene-target-select');
function _refreshTargetSelect() {
// Update add button disabled state
const addBtn = document.getElementById('scene-target-add-btn');
if (addBtn) {
const added = _getAddedTargetIds();
addBtn.disabled = _allTargets.every(t => added.has(t.id));
}
}
function _addTargetToList(targetId, targetName) {
const list = document.getElementById('scene-target-list');
if (!select || !list || !select.value) return;
const targetId = select.value;
const targetName = select.options[select.selectedIndex].text;
if (!list) return;
const item = document.createElement('div');
item.className = 'scene-target-item';
item.dataset.targetId = targetId;
item.innerHTML = `<span>${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">&#x2715;</button>`;
item.innerHTML = `<span>${ICON_TARGET} ${escapeHtml(targetName)}</span><button type="button" class="btn-remove-condition" onclick="removeSceneTarget(this)" title="Remove">&#x2715;</button>`;
list.appendChild(item);
_refreshTargetSelect();
}
export async function addSceneTarget() {
const added = _getAddedTargetIds();
const available = _allTargets.filter(t => !added.has(t.id));
if (available.length === 0) return;
const items = available.map(t => ({
value: t.id,
label: t.name,
icon: ICON_TARGET,
}));
const picked = await EntityPalette.pick({
items,
placeholder: t('scenes.targets.search_placeholder'),
});
if (!picked) return;
const tgt = _allTargets.find(t => t.id === picked);
if (tgt) _addTargetToList(tgt.id, tgt.name);
}
export function removeSceneTarget(btn) {
btn.closest('.scene-target-item').remove();
_refreshTargetSelect();