Frontend improvements: CSS foundations, accessibility, UX enhancements
CSS: Add design token variables (spacing, timing, weights, z-index layers), migrate all hardcoded z-index to named vars, fix light theme contrast for WCAG AA, add skeleton loading cards, mask-composite fallback, card padding. Accessibility: aria-live on toast, aria-label on health dots, sr-only class, graph container keyboard focusable, MQTT password wrapped in form element. UX: Modal auto-focus on open, inline field validation with blur, undo toast with countdown, bulk action progress indicator, API error toast on failure. i18n: Add common.undo, validation.required, bulk.processing, api.error.* keys in EN/RU/ZH. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -715,12 +715,10 @@ export async function loadTargetsTab() {
|
||||
changedTargetIds = new Set([...ledResult.added, ...ledResult.replaced, ...ledResult.removed,
|
||||
...kcResult.added, ...kcResult.replaced, ...kcResult.removed]);
|
||||
|
||||
// Re-render cached LED preview frames onto new canvas elements after reconciliation
|
||||
// Restore LED preview state on replaced cards (panel hidden by default in HTML)
|
||||
for (const id of Array.from(ledResult.replaced) as any[]) {
|
||||
const frame = _ledPreviewLastFrame[id];
|
||||
if (frame && ledPreviewWebSockets[id]) {
|
||||
const canvas = document.getElementById(`led-preview-canvas-${id}`);
|
||||
if (canvas) _renderLedStrip(canvas, frame);
|
||||
if (ledPreviewWebSockets[id]) {
|
||||
_restoreLedPreviewState(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1009,7 +1007,7 @@ export function createTargetCard(target: OutputTarget & { state?: any; metrics?:
|
||||
content: `
|
||||
<div class="card-header">
|
||||
<div class="card-title" title="${escapeHtml(target.name)}">
|
||||
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
||||
<span class="health-dot ${healthClass}" title="${healthTitle}" role="status" aria-label="${healthTitle}"></span>
|
||||
${escapeHtml(target.name)}
|
||||
<span class="target-error-indicator" title="${t('device.metrics.errors')}">${ICON_WARNING}</span>
|
||||
</div>
|
||||
@@ -1075,7 +1073,7 @@ export function createTargetCard(target: OutputTarget & { state?: any; metrics?:
|
||||
</button>
|
||||
`}
|
||||
${isProcessing ? `
|
||||
<button class="btn btn-icon ${ledPreviewWebSockets[target.id] ? 'btn-warning' : 'btn-secondary'}" onclick="toggleLedPreview('${target.id}')" title="LED Preview">
|
||||
<button class="btn btn-icon btn-secondary" data-led-preview-btn="${target.id}" onclick="toggleLedPreview('${target.id}')" title="LED Preview">
|
||||
${ICON_LED_PREVIEW}
|
||||
</button>
|
||||
` : ''}
|
||||
@@ -1234,7 +1232,9 @@ const _ledPreviewLastFrame = {};
|
||||
* one canvas per zone with labels. Otherwise, a single canvas.
|
||||
*/
|
||||
function _buildLedPreviewHtml(targetId: any, device: any, bvsId: any, cssSource: any, colorStripSourceMap: any) {
|
||||
const visible = ledPreviewWebSockets[targetId] ? '' : 'none';
|
||||
// Always render hidden — JS toggles visibility. This keeps card HTML stable
|
||||
// so reconciliation doesn't replace the card when preview is toggled.
|
||||
const visible = 'none';
|
||||
const bvsAttr = bvsId ? ' data-has-bvs="1"' : '';
|
||||
|
||||
// Check for per-zone preview
|
||||
@@ -1356,7 +1356,13 @@ function _renderLedStrip(canvas: any, rgbBytes: any) {
|
||||
}
|
||||
|
||||
function connectLedPreviewWS(targetId: any) {
|
||||
disconnectLedPreviewWS(targetId);
|
||||
// Close existing WS without touching DOM (caller manages panel/button state)
|
||||
const oldWs = ledPreviewWebSockets[targetId];
|
||||
if (oldWs) {
|
||||
oldWs.onclose = null;
|
||||
oldWs.close();
|
||||
delete ledPreviewWebSockets[targetId];
|
||||
}
|
||||
|
||||
const key = localStorage.getItem('wled_api_key');
|
||||
if (!key) return;
|
||||
@@ -1440,6 +1446,27 @@ function connectLedPreviewWS(targetId: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function _setPreviewButtonState(targetId: any, active: boolean) {
|
||||
const btn = document.querySelector(`[data-led-preview-btn="${targetId}"]`);
|
||||
if (btn) {
|
||||
btn.classList.toggle('btn-warning', active);
|
||||
btn.classList.toggle('btn-secondary', !active);
|
||||
}
|
||||
}
|
||||
|
||||
/** Restore preview panel visibility, button state, and last frame after card replacement. */
|
||||
function _restoreLedPreviewState(targetId: any) {
|
||||
const panel = document.getElementById(`led-preview-panel-${targetId}`);
|
||||
if (panel) panel.style.display = '';
|
||||
_setPreviewButtonState(targetId, true);
|
||||
// Re-render cached frame onto the new canvas
|
||||
const frame = _ledPreviewLastFrame[targetId];
|
||||
if (frame) {
|
||||
const canvas = panel?.querySelector('.led-preview-canvas');
|
||||
if (canvas) _renderLedStrip(canvas, frame);
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectLedPreviewWS(targetId: any) {
|
||||
const ws = ledPreviewWebSockets[targetId];
|
||||
if (ws) {
|
||||
@@ -1450,6 +1477,7 @@ function disconnectLedPreviewWS(targetId: any) {
|
||||
delete _ledPreviewLastFrame[targetId];
|
||||
const panel = document.getElementById(`led-preview-panel-${targetId}`);
|
||||
if (panel) panel.style.display = 'none';
|
||||
_setPreviewButtonState(targetId, false);
|
||||
}
|
||||
|
||||
export function disconnectAllLedPreviewWS() {
|
||||
@@ -1464,6 +1492,7 @@ export function toggleLedPreview(targetId: any) {
|
||||
disconnectLedPreviewWS(targetId);
|
||||
} else {
|
||||
panel.style.display = '';
|
||||
_setPreviewButtonState(targetId, true);
|
||||
connectLedPreviewWS(targetId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user