Comprehensive WebUI review: 41 UX/feature/CSS improvements
Safety & Correctness: - Add confirmation dialogs to Stop All, turnOffDevice - i18n confirm dialog (title, yes, no buttons) - Fix duplicate tutorial-overlay ID - Define missing CSS variables (--radius, --text-primary, --hover-bg, --input-bg) - Fix toast z-index conflict with confirm dialog (2500 → 3000) UX Consistency: - Add backdrop-close to test modals - Add device clone feature (only entity without it) - Add sync clocks to command palette - Replace 20+ hardcoded accent colors with CSS vars/color-mix() - Remove dead .badge duplicate from components.css - Make calibration elements keyboard-accessible (div → button) - Add aria-labels to color picker swatches - Fix pattern canvas mobile horizontal scroll - Fix graph editor mobile bottom clipping Polish: - Add empty-state messages to all CardSection instances - Convert 21 px font-sizes to rem - Add scroll-behavior: smooth with reduced-motion override - Add @media print styles - Add :focus-visible to 4 missing interactive elements - Fix settings modal close label (Cancel → Close) - Fix api-key submit button i18n New Features: - Command palette actions: start/stop targets, activate scenes, enable/disable - Bulk start/stop API endpoints (POST /output-targets/bulk/start|stop) - OS notification history viewer modal - Scene "used by" automation reference count on cards - Clock elapsed time display on Streams tab cards - Device "last seen" relative timestamp on cards - Audio device refresh button in edit modal - Composite layer drag-to-reorder - MQTT settings panel (broker config with JSON persistence) - WebSocket log viewer with level filtering and ring buffer - Runtime log-level adjustment (GET/PUT endpoints + settings UI) - Animated value source waveform canvas preview - Gradient custom preset save/delete (localStorage) - API key read-only display in settings - Backup metadata (file size, auto/manual badges) - Server restart button with confirm + overlay - Partial config export/import per entity type - Progressive disclosure in target editor (Advanced section) CSS Architecture: - Define radius scale tokens (--radius-sm/md/lg/pill) - Scope .cs-filter selectors to remove 7 !important overrides - Consolidate duplicate toggle switch (filter-list → settings-toggle) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,88 @@ function _ensureWaveformIconSelect() {
|
||||
_waveformIconSelect = new IconSelect({ target: sel, items, columns: 4 });
|
||||
}
|
||||
|
||||
/* ── Waveform canvas preview ──────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* Draw a waveform preview on the canvas element #value-source-waveform-preview.
|
||||
* Shows one full cycle of the selected waveform shape.
|
||||
*/
|
||||
function _drawWaveformPreview(waveformType) {
|
||||
const canvas = document.getElementById('value-source-waveform-preview');
|
||||
if (!canvas) return;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const cssW = canvas.offsetWidth || 200;
|
||||
const cssH = 60;
|
||||
canvas.width = cssW * dpr;
|
||||
canvas.height = cssH * dpr;
|
||||
canvas.style.height = cssH + 'px';
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, cssW, cssH);
|
||||
|
||||
const W = cssW;
|
||||
const H = cssH;
|
||||
const padX = 8;
|
||||
const padY = 8;
|
||||
const drawW = W - padX * 2;
|
||||
const drawH = H - padY * 2;
|
||||
const midY = padY + drawH / 2;
|
||||
|
||||
// Draw zero line
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.12)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([3, 4]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(padX, midY);
|
||||
ctx.lineTo(padX + drawW, midY);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Draw waveform
|
||||
const N = 120;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i / N; // 0..1 over one cycle
|
||||
let v; // -1..1
|
||||
switch (waveformType) {
|
||||
case 'triangle':
|
||||
v = t < 0.5 ? (4 * t - 1) : (3 - 4 * t);
|
||||
break;
|
||||
case 'square':
|
||||
v = t < 0.5 ? 1 : -1;
|
||||
break;
|
||||
case 'sawtooth':
|
||||
v = 2 * t - 1;
|
||||
break;
|
||||
case 'sine':
|
||||
default:
|
||||
v = Math.sin(2 * Math.PI * t);
|
||||
break;
|
||||
}
|
||||
const x = padX + t * drawW;
|
||||
const y = midY - v * (drawH / 2);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
|
||||
// Glow effect: draw thick translucent line first
|
||||
ctx.strokeStyle = 'rgba(99,179,237,0.25)';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.stroke();
|
||||
|
||||
// Crisp line on top
|
||||
ctx.strokeStyle = '#63b3ed';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
export function updateWaveformPreview() {
|
||||
const wf = document.getElementById('value-source-waveform')?.value || 'sine';
|
||||
_drawWaveformPreview(wf);
|
||||
}
|
||||
|
||||
/* ── Audio mode icon-grid selector ────────────────────────────── */
|
||||
|
||||
const _AUDIO_MODE_SVG = {
|
||||
@@ -208,6 +290,7 @@ export async function showValueSourceModal(editData, presetType = null) {
|
||||
} else if (editData.source_type === 'animated') {
|
||||
document.getElementById('value-source-waveform').value = editData.waveform || 'sine';
|
||||
if (_waveformIconSelect) _waveformIconSelect.setValue(editData.waveform || 'sine');
|
||||
_drawWaveformPreview(editData.waveform || 'sine');
|
||||
_setSlider('value-source-speed', editData.speed ?? 10);
|
||||
_setSlider('value-source-min-value', editData.min_value ?? 0);
|
||||
_setSlider('value-source-max-value', editData.max_value ?? 1);
|
||||
@@ -249,6 +332,7 @@ export async function showValueSourceModal(editData, presetType = null) {
|
||||
_setSlider('value-source-min-value', 0);
|
||||
_setSlider('value-source-max-value', 1);
|
||||
document.getElementById('value-source-waveform').value = 'sine';
|
||||
_drawWaveformPreview('sine');
|
||||
_populateAudioSourceDropdown('');
|
||||
document.getElementById('value-source-mode').value = 'rms';
|
||||
if (_audioModeIconSelect) _audioModeIconSelect.setValue('rms');
|
||||
@@ -274,7 +358,7 @@ export async function showValueSourceModal(editData, presetType = null) {
|
||||
}
|
||||
|
||||
// Wire up auto-name triggers
|
||||
document.getElementById('value-source-waveform').onchange = () => _autoGenerateVSName();
|
||||
document.getElementById('value-source-waveform').onchange = () => { _autoGenerateVSName(); _drawWaveformPreview(document.getElementById('value-source-waveform').value); };
|
||||
document.getElementById('value-source-mode').onchange = () => _autoGenerateVSName();
|
||||
document.getElementById('value-source-picture-source').onchange = () => _autoGenerateVSName();
|
||||
|
||||
@@ -296,7 +380,7 @@ export function onValueSourceTypeChange() {
|
||||
if (_vsTypeIconSelect) _vsTypeIconSelect.setValue(type);
|
||||
document.getElementById('value-source-static-section').style.display = type === 'static' ? '' : 'none';
|
||||
document.getElementById('value-source-animated-section').style.display = type === 'animated' ? '' : 'none';
|
||||
if (type === 'animated') _ensureWaveformIconSelect();
|
||||
if (type === 'animated') { _ensureWaveformIconSelect(); _drawWaveformPreview(document.getElementById('value-source-waveform').value); }
|
||||
document.getElementById('value-source-audio-section').style.display = type === 'audio' ? '' : 'none';
|
||||
if (type === 'audio') _ensureAudioModeIconSelect();
|
||||
document.getElementById('value-source-adaptive-time-section').style.display = type === 'adaptive_time' ? '' : 'none';
|
||||
|
||||
Reference in New Issue
Block a user