fix: UI polish — notification history buttons, CSS test error handling, schedule time picker, empty state labels
All checks were successful
Lint & Test / test (push) Successful in 31s

- Notification history: replace text buttons with icon buttons, use modal-footer for proper positioning
- CSS test: reject 0-LED picture sources with clear error message, show WS close reason in UI
- Calibration: distribute LEDs by aspect ratio (16:9 default) instead of evenly across edges
- Value source schedule: replace native time input with custom HH:MM picker matching automation style
- Remove "No ... yet" empty state labels from all CardSection instances
This commit is contained in:
2026-03-22 20:58:13 +03:00
parent 52c8614a3c
commit f376622482
7 changed files with 201 additions and 39 deletions

View File

@@ -383,10 +383,18 @@ function _cssTestConnect(sourceId: string, ledCount: number, fps?: number) {
_cssTestWs.onerror = () => {
if (gen !== _cssTestGeneration) return;
(document.getElementById('css-test-status') as HTMLElement).textContent = t('color_strip.test.error');
(document.getElementById('css-test-status') as HTMLElement).style.display = '';
};
_cssTestWs.onclose = () => {
if (gen === _cssTestGeneration) _cssTestWs = null;
_cssTestWs.onclose = (ev) => {
if (gen !== _cssTestGeneration) return;
_cssTestWs = null;
// Show server-provided close reason (e.g. "No LEDs configured")
if (ev.reason) {
const statusEl = document.getElementById('css-test-status') as HTMLElement;
statusEl.textContent = ev.reason;
statusEl.style.display = '';
}
};
// Start render loop (only once)

View File

@@ -874,16 +874,73 @@ function _populatePictureSourceDropdown(selectedId: any) {
export function addSchedulePoint(time: string = '', value: number = 1.0) {
const list = document.getElementById('value-source-schedule-list');
if (!list) return;
const timeStr = time || '12:00';
const [h, m] = timeStr.split(':').map(Number);
const pad = (n: number) => String(n).padStart(2, '0');
const row = document.createElement('div');
row.className = 'schedule-row';
row.innerHTML = `
<input type="time" class="schedule-time" value="${time || '12:00'}">
<div class="schedule-time-wrap">
<input type="number" class="schedule-h" min="0" max="23" value="${pad(h)}" data-role="hour">
<span class="schedule-time-colon">:</span>
<input type="number" class="schedule-m" min="0" max="59" value="${pad(m)}" data-role="minute">
</div>
<input type="hidden" class="schedule-time" value="${timeStr}">
<input type="range" class="schedule-value" min="0" max="1" step="0.01" value="${value}"
oninput="this.nextElementSibling.textContent = this.value">
<span class="schedule-value-display">${value}</span>
<button type="button" class="btn btn-icon btn-danger btn-sm" onclick="this.parentElement.remove()">&#x2715;</button>
`;
list.appendChild(row);
_wireScheduleTimePicker(row);
}
function _wireScheduleTimePicker(row: HTMLElement) {
const hInput = row.querySelector('.schedule-h') as HTMLInputElement;
const mInput = row.querySelector('.schedule-m') as HTMLInputElement;
const hidden = row.querySelector('.schedule-time') as HTMLInputElement;
if (!hInput || !mInput || !hidden) return;
const pad = (n: number) => String(n).padStart(2, '0');
function clamp(input: HTMLInputElement, min: number, max: number) {
let v = parseInt(input.value, 10);
if (isNaN(v)) v = min;
if (v < min) v = min;
if (v > max) v = max;
input.value = pad(v);
return v;
}
function sync() {
const hv = clamp(hInput, 0, 23);
const mv = clamp(mInput, 0, 59);
hidden.value = `${pad(hv)}:${pad(mv)}`;
}
[hInput, mInput].forEach(inp => {
inp.addEventListener('focus', () => inp.select());
inp.addEventListener('input', sync);
inp.addEventListener('blur', sync);
inp.addEventListener('keydown', (e) => {
const isHour = inp.dataset.role === 'hour';
const max = isHour ? 23 : 59;
if (e.key === 'ArrowUp') {
e.preventDefault();
let v = parseInt(inp.value, 10) || 0;
inp.value = pad(v >= max ? 0 : v + 1);
sync();
} else if (e.key === 'ArrowDown') {
e.preventDefault();
let v = parseInt(inp.value, 10) || 0;
inp.value = pad(v <= 0 ? max : v - 1);
sync();
}
});
});
sync();
}
function _getScheduleFromUI() {