Replace bare — and generic None in selectors with descriptive None (reason) labels

All optional entity selectors now use the format "None (description)" with
i18n keys instead of hardcoded "—" or bare "None". Added common.none_no_cspt,
common.none_no_input, common.none_own_speed keys to all 3 locales. Updated
frontend context with the convention.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 17:46:37 +03:00
parent bbe42ee0a2
commit a4a0e39b9b
7 changed files with 38 additions and 16 deletions

View File

@@ -55,6 +55,19 @@ Add hint text to both `en.json` and `ru.json` locale files using a `.hint` suffi
Do **not** add placeholder options like `-- Select something --`. Populate the `<select>` with real options only and let the first one be selected by default. Do **not** add placeholder options like `-- Select something --`. Populate the `<select>` with real options only and let the first one be selected by default.
### Empty/None option format
When a selector has an optional entity (e.g., sync clock, processing template, brightness source), the empty option must use the format `None (<description>)` where the description explains what happens when nothing is selected. Use i18n keys, never hardcoded `—` or bare `None`.
Examples:
- `None (no processing template)``t('common.none_no_cspt')`
- `None (no input source)``t('common.none_no_input')`
- `None (use own speed)``t('common.none_own_speed')`
- `None (full brightness)``t('color_strip.composite.brightness.none')`
- `None (device brightness)``t('targets.brightness_vs.none')`
For `EntitySelect` with `allowNone: true`, pass the same i18n string as `noneLabel`.
### Enhanced selectors (IconSelect & EntitySelect) ### Enhanced selectors (IconSelect & EntitySelect)
Plain `<select>` dropdowns should be enhanced with visual selectors depending on the data type: Plain `<select>` dropdowns should be enhanced with visual selectors depending on the data type:

View File

@@ -222,7 +222,7 @@ export function onCSSTypeChange() {
function _populateClockDropdown(selectedId) { function _populateClockDropdown(selectedId) {
const sel = document.getElementById('css-editor-clock'); const sel = document.getElementById('css-editor-clock');
const prev = selectedId !== undefined ? selectedId : sel.value; const prev = selectedId !== undefined ? selectedId : sel.value;
sel.innerHTML = `<option value="">${t('common.none')}</option>` + sel.innerHTML = `<option value="">${t('common.none_own_speed')}</option>` +
_cachedSyncClocks.map(c => `<option value="${c.id}">${escapeHtml(c.name)} (${c.speed}x)</option>`).join(''); _cachedSyncClocks.map(c => `<option value="${c.id}">${escapeHtml(c.name)} (${c.speed}x)</option>`).join('');
sel.value = prev || ''; sel.value = prev || '';
@@ -238,7 +238,7 @@ function _populateClockDropdown(selectedId) {
})), })),
placeholder: t('palette.search'), placeholder: t('palette.search'),
allowNone: true, allowNone: true,
noneLabel: t('common.none'), noneLabel: t('common.none_own_speed'),
}); });
} }
@@ -253,7 +253,7 @@ function _populateProcessedSelectors() {
const inputSources = allSources.filter(s => s.id !== editingId && s.source_type !== 'processed'); const inputSources = allSources.filter(s => s.id !== editingId && s.source_type !== 'processed');
const inputSel = document.getElementById('css-editor-processed-input'); const inputSel = document.getElementById('css-editor-processed-input');
const prevInput = inputSel.value; const prevInput = inputSel.value;
inputSel.innerHTML = '<option value=""></option>' + inputSel.innerHTML = `<option value="">${t('common.none_no_input')}</option>` +
inputSources.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join(''); inputSources.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
inputSel.value = prevInput || ''; inputSel.value = prevInput || '';
if (_processedInputEntitySelect) _processedInputEntitySelect.destroy(); if (_processedInputEntitySelect) _processedInputEntitySelect.destroy();
@@ -270,7 +270,7 @@ function _populateProcessedSelectors() {
const templates = csptCache.data || []; const templates = csptCache.data || [];
const tplSel = document.getElementById('css-editor-processed-template'); const tplSel = document.getElementById('css-editor-processed-template');
const prevTpl = tplSel.value; const prevTpl = tplSel.value;
tplSel.innerHTML = '<option value=""></option>' + tplSel.innerHTML = `<option value="">${t('common.none_no_cspt')}</option>` +
templates.map(tp => `<option value="${tp.id}">${escapeHtml(tp.name)}</option>`).join(''); templates.map(tp => `<option value="${tp.id}">${escapeHtml(tp.name)}</option>`).join('');
tplSel.value = prevTpl || ''; tplSel.value = prevTpl || '';
if (_processedTemplateEntitySelect) _processedTemplateEntitySelect.destroy(); if (_processedTemplateEntitySelect) _processedTemplateEntitySelect.destroy();
@@ -644,7 +644,7 @@ function _compositeRenderList() {
`<option value="${v.id}"${layer.brightness_source_id === v.id ? ' selected' : ''}>${escapeHtml(v.name)}</option>` `<option value="${v.id}"${layer.brightness_source_id === v.id ? ' selected' : ''}>${escapeHtml(v.name)}</option>`
).join(''); ).join('');
const csptList = _cachedCSPTemplates || []; const csptList = _cachedCSPTemplates || [];
const csptOptions = `<option value="">${t('common.none')}</option>` + const csptOptions = `<option value="">${t('common.none_no_cspt')}</option>` +
csptList.map(tmpl => csptList.map(tmpl =>
`<option value="${tmpl.id}"${layer.processing_template_id === tmpl.id ? ' selected' : ''}>${escapeHtml(tmpl.name)}</option>` `<option value="${tmpl.id}"${layer.processing_template_id === tmpl.id ? ' selected' : ''}>${escapeHtml(tmpl.name)}</option>`
).join(''); ).join('');
@@ -734,7 +734,7 @@ function _compositeRenderList() {
getItems: _getCompositeCSPTItems, getItems: _getCompositeCSPTItems,
placeholder: t('palette.search'), placeholder: t('palette.search'),
allowNone: true, allowNone: true,
noneLabel: t('common.none'), noneLabel: t('common.none_no_cspt'),
})); }));
}); });
} }

View File

@@ -70,7 +70,7 @@ function _ensureCsptEntitySelect() {
if (!sel) return; if (!sel) return;
const templates = csptCache.data || []; const templates = csptCache.data || [];
// Populate native <select> options // Populate native <select> options
sel.innerHTML = `<option value=""></option>` + sel.innerHTML = `<option value="">${t('common.none_no_cspt')}</option>` +
templates.map(tp => `<option value="${tp.id}">${tp.name}</option>`).join(''); templates.map(tp => `<option value="${tp.id}">${tp.name}</option>`).join('');
if (_csptEntitySelect) _csptEntitySelect.destroy(); if (_csptEntitySelect) _csptEntitySelect.destroy();
if (templates.length > 0) { if (templates.length > 0) {
@@ -84,7 +84,7 @@ function _ensureCsptEntitySelect() {
})), })),
placeholder: t('palette.search'), placeholder: t('palette.search'),
allowNone: true, allowNone: true,
noneLabel: '—', noneLabel: t('common.none_no_cspt'),
}); });
} }
} }

View File

@@ -24,7 +24,7 @@ function _ensureSettingsCsptSelect() {
const sel = document.getElementById('settings-css-processing-template'); const sel = document.getElementById('settings-css-processing-template');
if (!sel) return; if (!sel) return;
const templates = csptCache.data || []; const templates = csptCache.data || [];
sel.innerHTML = `<option value=""></option>` + sel.innerHTML = `<option value="">${t('common.none_no_cspt')}</option>` +
templates.map(tp => `<option value="${tp.id}">${tp.name}</option>`).join(''); templates.map(tp => `<option value="${tp.id}">${tp.name}</option>`).join('');
if (_settingsCsptEntitySelect) _settingsCsptEntitySelect.destroy(); if (_settingsCsptEntitySelect) _settingsCsptEntitySelect.destroy();
if (templates.length > 0) { if (templates.length > 0) {
@@ -38,7 +38,7 @@ function _ensureSettingsCsptSelect() {
})), })),
placeholder: window.t ? t('palette.search') : 'Search...', placeholder: window.t ? t('palette.search') : 'Search...',
allowNone: true, allowNone: true,
noneLabel: '—', noneLabel: t('common.none_no_cspt'),
}); });
} }
} }

View File

@@ -410,6 +410,9 @@
"common.edit": "Edit", "common.edit": "Edit",
"common.clone": "Clone", "common.clone": "Clone",
"common.none": "None", "common.none": "None",
"common.none_no_cspt": "None (no processing template)",
"common.none_no_input": "None (no input source)",
"common.none_own_speed": "None (use own speed)",
"palette.search": "Search…", "palette.search": "Search…",
"section.filter.placeholder": "Filter...", "section.filter.placeholder": "Filter...",
"section.filter.reset": "Clear filter", "section.filter.reset": "Clear filter",
@@ -803,7 +806,7 @@
"automations.scene": "Scene:", "automations.scene": "Scene:",
"automations.scene.hint": "Scene preset to activate when conditions are met", "automations.scene.hint": "Scene preset to activate when conditions are met",
"automations.scene.search_placeholder": "Search scenes...", "automations.scene.search_placeholder": "Search scenes...",
"automations.scene.none_selected": "No scene", "automations.scene.none_selected": "None (no scene)",
"automations.scene.none_available": "No scenes available", "automations.scene.none_available": "No scenes available",
"automations.deactivation_mode": "Deactivation:", "automations.deactivation_mode": "Deactivation:",
"automations.deactivation_mode.hint": "What happens when conditions stop matching", "automations.deactivation_mode.hint": "What happens when conditions stop matching",
@@ -1097,7 +1100,7 @@
"color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply", "color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply",
"color_strip.composite.opacity": "Opacity", "color_strip.composite.opacity": "Opacity",
"color_strip.composite.brightness": "Brightness", "color_strip.composite.brightness": "Brightness",
"color_strip.composite.brightness.none": "None ", "color_strip.composite.brightness.none": "None (full brightness)",
"color_strip.composite.processing": "Processing", "color_strip.composite.processing": "Processing",
"color_strip.composite.enabled": "Enabled", "color_strip.composite.enabled": "Enabled",
"color_strip.composite.error.min_layers": "At least 1 layer is required", "color_strip.composite.error.min_layers": "At least 1 layer is required",

View File

@@ -410,6 +410,9 @@
"common.edit": "Редактировать", "common.edit": "Редактировать",
"common.clone": "Клонировать", "common.clone": "Клонировать",
"common.none": "Нет", "common.none": "Нет",
"common.none_no_cspt": "Нет (без шаблона обработки)",
"common.none_no_input": "Нет (без источника)",
"common.none_own_speed": "Нет (своя скорость)",
"palette.search": "Поиск…", "palette.search": "Поиск…",
"section.filter.placeholder": "Фильтр...", "section.filter.placeholder": "Фильтр...",
"section.filter.reset": "Очистить фильтр", "section.filter.reset": "Очистить фильтр",
@@ -803,7 +806,7 @@
"automations.scene": "Сцена:", "automations.scene": "Сцена:",
"automations.scene.hint": "Пресет сцены для активации при выполнении условий", "automations.scene.hint": "Пресет сцены для активации при выполнении условий",
"automations.scene.search_placeholder": "Поиск сцен...", "automations.scene.search_placeholder": "Поиск сцен...",
"automations.scene.none_selected": "Нет сцены", "automations.scene.none_selected": "Нет (без сцены)",
"automations.scene.none_available": "Нет доступных сцен", "automations.scene.none_available": "Нет доступных сцен",
"automations.deactivation_mode": "Деактивация:", "automations.deactivation_mode": "Деактивация:",
"automations.deactivation_mode.hint": "Что происходит, когда условия перестают выполняться", "automations.deactivation_mode.hint": "Что происходит, когда условия перестают выполняться",
@@ -1097,7 +1100,7 @@
"color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение", "color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение",
"color_strip.composite.opacity": "Непрозрачность", "color_strip.composite.opacity": "Непрозрачность",
"color_strip.composite.brightness": "Яркость", "color_strip.composite.brightness": "Яркость",
"color_strip.composite.brightness.none": "Нет ", "color_strip.composite.brightness.none": "Нет (полная яркость)",
"color_strip.composite.processing": "Обработка", "color_strip.composite.processing": "Обработка",
"color_strip.composite.enabled": "Включён", "color_strip.composite.enabled": "Включён",
"color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой", "color_strip.composite.error.min_layers": "Необходим хотя бы 1 слой",

View File

@@ -410,6 +410,9 @@
"common.edit": "编辑", "common.edit": "编辑",
"common.clone": "克隆", "common.clone": "克隆",
"common.none": "无", "common.none": "无",
"common.none_no_cspt": "无(无处理模板)",
"common.none_no_input": "无(无输入源)",
"common.none_own_speed": "无(使用自身速度)",
"palette.search": "搜索…", "palette.search": "搜索…",
"section.filter.placeholder": "筛选...", "section.filter.placeholder": "筛选...",
"section.filter.reset": "清除筛选", "section.filter.reset": "清除筛选",
@@ -803,7 +806,7 @@
"automations.scene": "场景:", "automations.scene": "场景:",
"automations.scene.hint": "条件满足时激活的场景预设", "automations.scene.hint": "条件满足时激活的场景预设",
"automations.scene.search_placeholder": "搜索场景...", "automations.scene.search_placeholder": "搜索场景...",
"automations.scene.none_selected": "无场景", "automations.scene.none_selected": "无(无场景",
"automations.scene.none_available": "没有可用的场景", "automations.scene.none_available": "没有可用的场景",
"automations.deactivation_mode": "停用方式:", "automations.deactivation_mode": "停用方式:",
"automations.deactivation_mode.hint": "条件不再满足时的行为", "automations.deactivation_mode.hint": "条件不再满足时的行为",
@@ -1097,7 +1100,7 @@
"color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转", "color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转",
"color_strip.composite.opacity": "不透明度", "color_strip.composite.opacity": "不透明度",
"color_strip.composite.brightness": "亮度", "color_strip.composite.brightness": "亮度",
"color_strip.composite.brightness.none": "— 无 —", "color_strip.composite.brightness.none": "无(全亮度)",
"color_strip.composite.processing": "处理", "color_strip.composite.processing": "处理",
"color_strip.composite.enabled": "启用", "color_strip.composite.enabled": "启用",
"color_strip.composite.error.min_layers": "至少需要 1 个图层", "color_strip.composite.error.min_layers": "至少需要 1 个图层",