Fix UI review issues: accessibility, i18n, duplicate IDs, URL overflow

- Rename duplicate id="settings-error" to "device-settings-error"
- Add missing i18n key value_source.scene_sensitivity.hint (en/ru/zh)
- Add accessible label to password-toggle and Stop All buttons
- Add aria-hidden toggle on connection overlay
- Fix static image URL overflow with ellipsis truncation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:37:56 +03:00
parent 6a22757755
commit bf910204a9
9 changed files with 23 additions and 12 deletions

View File

@@ -59,9 +59,11 @@
} }
.stream-card-prop-full { .stream-card-prop-full {
max-width: 100%; flex: 1 1 100%;
word-break: break-all; min-width: 0;
white-space: normal; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.7rem; font-size: 0.7rem;
} }

View File

@@ -129,10 +129,10 @@ function _setConnectionState(online) {
const banner = document.getElementById('connection-overlay'); const banner = document.getElementById('connection-overlay');
const badge = document.getElementById('server-status'); const badge = document.getElementById('server-status');
if (online) { if (online) {
if (banner) banner.style.display = 'none'; if (banner) { banner.style.display = 'none'; banner.setAttribute('aria-hidden', 'true'); }
if (badge) badge.className = 'status-badge online'; if (badge) badge.className = 'status-badge online';
} else { } else {
if (banner) banner.style.display = 'flex'; if (banner) { banner.style.display = 'flex'; banner.setAttribute('aria-hidden', 'false'); }
if (badge) badge.className = 'status-badge offline'; if (badge) badge.className = 'status-badge offline';
} }
return changed; return changed;

View File

@@ -40,8 +40,8 @@ import { updateSubTabHash, updateTabBadge } from './tabs.js';
// ── Card section instances ── // ── Card section instances ──
const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()", keyAttr: 'data-device-id' }); const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()", keyAttr: 'data-device-id' });
const csColorStrips = new CardSection('led-css', { titleKey: 'targets.section.color_strips', gridClass: 'devices-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id' }); const csColorStrips = new CardSection('led-css', { titleKey: 'targets.section.color_strips', gridClass: 'devices-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id' });
const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllLedTargets()" data-stop-all="led">${ICON_STOP}</button>` }); const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()", keyAttr: 'data-target-id', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllLedTargets()" data-stop-all="led" data-i18n-title="targets.stop_all.button" data-i18n-aria-label="targets.stop_all.button">${ICON_STOP}</button>` });
const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllKCTargets()" data-stop-all="kc">${ICON_STOP}</button>` }); const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()", keyAttr: 'data-kc-target-id', headerExtra: `<button class="btn btn-sm btn-danger" onclick="event.stopPropagation(); stopAllKCTargets()" data-stop-all="kc" data-i18n-title="targets.stop_all.button" data-i18n-aria-label="targets.stop_all.button">${ICON_STOP}</button>` });
const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()", keyAttr: 'data-pattern-template-id' }); const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()", keyAttr: 'data-pattern-template-id' });
// Re-render targets tab when language changes (only if tab is active) // Re-render targets tab when language changes (only if tab is active)
@@ -731,8 +731,8 @@ export async function loadTargetsTab() {
const kcRunning = kcTargets.some(t => t.state && t.state.processing); const kcRunning = kcTargets.some(t => t.state && t.state.processing);
const ledStopBtn = container.querySelector('[data-stop-all="led"]'); const ledStopBtn = container.querySelector('[data-stop-all="led"]');
const kcStopBtn = container.querySelector('[data-stop-all="kc"]'); const kcStopBtn = container.querySelector('[data-stop-all="kc"]');
if (ledStopBtn) ledStopBtn.style.display = ledRunning ? '' : 'none'; if (ledStopBtn) { ledStopBtn.style.display = ledRunning ? '' : 'none'; if (!ledStopBtn.title) { ledStopBtn.title = t('targets.stop_all.button'); ledStopBtn.setAttribute('aria-label', t('targets.stop_all.button')); } }
if (kcStopBtn) kcStopBtn.style.display = kcRunning ? '' : 'none'; if (kcStopBtn) { kcStopBtn.style.display = kcRunning ? '' : 'none'; if (!kcStopBtn.title) { kcStopBtn.title = t('targets.stop_all.button'); kcStopBtn.setAttribute('aria-label', t('targets.stop_all.button')); } }
// Patch volatile metrics in-place (avoids full card replacement on polls) // Patch volatile metrics in-place (avoids full card replacement on polls)
for (const tgt of ledTargets) { for (const tgt of ledTargets) {

View File

@@ -25,6 +25,7 @@
"auth.logout.success": "Logged out successfully", "auth.logout.success": "Logged out successfully",
"auth.please_login": "Please login to view", "auth.please_login": "Please login to view",
"auth.session_expired": "Your session has expired or the API key is invalid. Please login again.", "auth.session_expired": "Your session has expired or the API key is invalid. Please login again.",
"auth.toggle_password": "Toggle password visibility",
"displays.title": "Available Displays", "displays.title": "Available Displays",
"displays.layout": "Displays", "displays.layout": "Displays",
"displays.information": "Display Information", "displays.information": "Display Information",
@@ -1162,6 +1163,7 @@
"value_source.auto_gain.enable": "Enable auto-gain", "value_source.auto_gain.enable": "Enable auto-gain",
"value_source.sensitivity": "Sensitivity:", "value_source.sensitivity": "Sensitivity:",
"value_source.sensitivity.hint": "Gain multiplier for the audio signal (higher = more reactive)", "value_source.sensitivity.hint": "Gain multiplier for the audio signal (higher = more reactive)",
"value_source.scene_sensitivity.hint": "Gain multiplier for the luminance signal (higher = more reactive to brightness changes)",
"value_source.smoothing": "Smoothing:", "value_source.smoothing": "Smoothing:",
"value_source.smoothing.hint": "Temporal smoothing (0 = instant response, 1 = very smooth/slow)", "value_source.smoothing.hint": "Temporal smoothing (0 = instant response, 1 = very smooth/slow)",
"value_source.audio_min_value": "Min Value:", "value_source.audio_min_value": "Min Value:",
@@ -1309,6 +1311,7 @@
"target.error.stop_failed": "Failed to stop target", "target.error.stop_failed": "Failed to stop target",
"target.error.clone_failed": "Failed to clone target", "target.error.clone_failed": "Failed to clone target",
"target.error.delete_failed": "Failed to delete target", "target.error.delete_failed": "Failed to delete target",
"targets.stop_all.button": "Stop All",
"targets.stop_all.none_running": "No targets are currently running", "targets.stop_all.none_running": "No targets are currently running",
"targets.stop_all.stopped": "Stopped {count} target(s)", "targets.stop_all.stopped": "Stopped {count} target(s)",
"targets.stop_all.error": "Failed to stop targets", "targets.stop_all.error": "Failed to stop targets",

View File

@@ -25,6 +25,7 @@
"auth.logout.success": "Выход выполнен успешно", "auth.logout.success": "Выход выполнен успешно",
"auth.please_login": "Пожалуйста, войдите для просмотра", "auth.please_login": "Пожалуйста, войдите для просмотра",
"auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.", "auth.session_expired": "Ваша сессия истекла или API ключ недействителен. Пожалуйста, войдите снова.",
"auth.toggle_password": "Показать/скрыть пароль",
"displays.title": "Доступные Дисплеи", "displays.title": "Доступные Дисплеи",
"displays.layout": "Дисплеи", "displays.layout": "Дисплеи",
"displays.information": "Информация о Дисплеях", "displays.information": "Информация о Дисплеях",
@@ -1162,6 +1163,7 @@
"value_source.auto_gain.enable": "Включить авто-усиление", "value_source.auto_gain.enable": "Включить авто-усиление",
"value_source.sensitivity": "Чувствительность:", "value_source.sensitivity": "Чувствительность:",
"value_source.sensitivity.hint": "Множитель усиления аудиосигнала (выше = более реактивный)", "value_source.sensitivity.hint": "Множитель усиления аудиосигнала (выше = более реактивный)",
"value_source.scene_sensitivity.hint": "Множитель усиления сигнала яркости (выше = более чувствительный к изменениям яркости)",
"value_source.smoothing": "Сглаживание:", "value_source.smoothing": "Сглаживание:",
"value_source.smoothing.hint": "Временное сглаживание (0 = мгновенный отклик, 1 = очень плавный/медленный)", "value_source.smoothing.hint": "Временное сглаживание (0 = мгновенный отклик, 1 = очень плавный/медленный)",
"value_source.audio_min_value": "Мин. значение:", "value_source.audio_min_value": "Мин. значение:",
@@ -1309,6 +1311,7 @@
"target.error.stop_failed": "Не удалось остановить цель", "target.error.stop_failed": "Не удалось остановить цель",
"target.error.clone_failed": "Не удалось клонировать цель", "target.error.clone_failed": "Не удалось клонировать цель",
"target.error.delete_failed": "Не удалось удалить цель", "target.error.delete_failed": "Не удалось удалить цель",
"targets.stop_all.button": "Остановить все",
"targets.stop_all.none_running": "Нет запущенных целей", "targets.stop_all.none_running": "Нет запущенных целей",
"targets.stop_all.stopped": "Остановлено целей: {count}", "targets.stop_all.stopped": "Остановлено целей: {count}",
"targets.stop_all.error": "Не удалось остановить цели", "targets.stop_all.error": "Не удалось остановить цели",

View File

@@ -25,6 +25,7 @@
"auth.logout.success": "已成功退出", "auth.logout.success": "已成功退出",
"auth.please_login": "请先登录", "auth.please_login": "请先登录",
"auth.session_expired": "会话已过期或 API 密钥无效,请重新登录。", "auth.session_expired": "会话已过期或 API 密钥无效,请重新登录。",
"auth.toggle_password": "切换密码可见性",
"displays.title": "可用显示器", "displays.title": "可用显示器",
"displays.layout": "显示器", "displays.layout": "显示器",
"displays.information": "显示器信息", "displays.information": "显示器信息",
@@ -1162,6 +1163,7 @@
"value_source.auto_gain.enable": "启用自动增益", "value_source.auto_gain.enable": "启用自动增益",
"value_source.sensitivity": "灵敏度:", "value_source.sensitivity": "灵敏度:",
"value_source.sensitivity.hint": "音频信号的增益倍数(越高反应越灵敏)", "value_source.sensitivity.hint": "音频信号的增益倍数(越高反应越灵敏)",
"value_source.scene_sensitivity.hint": "亮度信号的增益倍数(越高对亮度变化越敏感)",
"value_source.smoothing": "平滑:", "value_source.smoothing": "平滑:",
"value_source.smoothing.hint": "时间平滑0 = 即时响应1 = 非常平滑/缓慢)", "value_source.smoothing.hint": "时间平滑0 = 即时响应1 = 非常平滑/缓慢)",
"value_source.audio_min_value": "最小值:", "value_source.audio_min_value": "最小值:",
@@ -1309,6 +1311,7 @@
"target.error.stop_failed": "停止目标失败", "target.error.stop_failed": "停止目标失败",
"target.error.clone_failed": "克隆目标失败", "target.error.clone_failed": "克隆目标失败",
"target.error.delete_failed": "删除目标失败", "target.error.delete_failed": "删除目标失败",
"targets.stop_all.button": "全部停止",
"targets.stop_all.none_running": "当前没有运行中的目标", "targets.stop_all.none_running": "当前没有运行中的目标",
"targets.stop_all.stopped": "已停止 {count} 个目标", "targets.stop_all.stopped": "已停止 {count} 个目标",
"targets.stop_all.error": "停止目标失败", "targets.stop_all.error": "停止目标失败",

View File

@@ -29,7 +29,7 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
</head> </head>
<body style="visibility: hidden;"> <body style="visibility: hidden;">
<div id="connection-overlay" class="connection-overlay" style="display:none"> <div id="connection-overlay" class="connection-overlay" style="display:none" aria-hidden="true">
<div class="connection-overlay-content"> <div class="connection-overlay-content">
<div class="connection-spinner-lg"></div> <div class="connection-spinner-lg"></div>
<h2 data-i18n="app.connection_lost">Server unreachable</h2> <h2 data-i18n="app.connection_lost">Server unreachable</h2>

View File

@@ -24,7 +24,7 @@
placeholder="Enter your API key..." placeholder="Enter your API key..."
autocomplete="off" autocomplete="off"
> >
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()"> <button type="button" class="password-toggle" onclick="togglePasswordVisibility()" title="Toggle password visibility" data-i18n-title="auth.toggle_password" aria-label="Toggle password visibility" data-i18n-aria-label="auth.toggle_password">
<svg class="icon" viewBox="0 0 24 24"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg> <svg class="icon" viewBox="0 0 24 24"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>
</button> </button>
</div> </div>

View File

@@ -137,7 +137,7 @@
</div> </div>
</div> </div>
<div id="settings-error" class="error-message" style="display: none;"></div> <div id="device-settings-error" class="error-message" style="display: none;"></div>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">