feat(web): floating live preview + per-plugin "Preview on display", drawer a11y

Preview-while-configuring:
- Floating mini preview (fixed, bottom-right) available on every tab except
  Overview, fed from the same SSE display stream by updateDisplayPreview -
  no new connections. Collapses to a round toggle button; open/closed state
  persists in localStorage; hides on Overview where the full preview lives.
- "Preview on display" button on every plugin config page header: runs that
  plugin on the real display for 60 seconds via the existing
  /display/on-demand/start API and opens the floating preview, closing the
  configure -> see-the-result loop.

Drawer/nav accessibility:
- aria-current="page" tracks the active tab (system + dynamic plugin tabs,
  matched via their Alpine @click expression), updated from the activeTab
  watcher so search deep-links and checklist navigation are covered too.
- Escape closes the mobile drawer and returns focus to the hamburger;
  opening the drawer moves focus to its first tab.

Validation: all 40 web tests pass; Jinja parse + div balance on both
touched templates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
ChuckBuilds
2026-07-16 12:27:36 -04:00
co-authored by Claude Sonnet 5
parent 23dcdc2d49
commit e1e26e8eab
5 changed files with 184 additions and 2 deletions
+24 -2
View File
@@ -312,15 +312,29 @@
if (typeof this.updatePluginTabStates === 'function') {
this.updatePluginTabStates();
}
// Screen readers announce the current tab (covers every
// path that changes tabs: clicks, search deep links,
// the getting-started checklist)
if (typeof window.updateNavAriaCurrent === 'function') {
window.updateNavAriaCurrent(newTab);
}
// Floating preview hides on Overview (full preview
// there), reappears per its saved state elsewhere
if (typeof window.updateFloatingPreviewVisibility === 'function') {
window.updateFloatingPreviewVisibility(newTab);
}
// Trigger content load when tab changes
this.$nextTick(() => {
this.loadTabContent(newTab);
});
});
// Load initial tab content
this.$nextTick(() => {
this.loadTabContent(this.activeTab);
if (typeof window.updateNavAriaCurrent === 'function') {
window.updateNavAriaCurrent(this.activeTab);
}
});
// Listen for plugin updates from pluginManager
@@ -1860,7 +1874,15 @@
const canvas = document.getElementById('gridOverlay');
const ledCanvas = document.getElementById('ledCanvas');
const placeholder = document.getElementById('displayPlaceholder');
// Feed the floating mini preview (lives in base.html, present on
// every tab) before the overview-only guard below.
const floatImg = document.getElementById('floating-preview-img');
const floatPanel = document.getElementById('floating-preview');
if (floatImg && floatPanel && floatPanel.style.display !== 'none' && data.image) {
floatImg.src = `data:image/png;base64,${data.image}`;
}
if (!stage || !img || !placeholder) return; // Not on overview page
if (data.image) {