feat(ui): refresh layout, help assets, and panel styling

Update the main template and client scripts for the revised navigation
and zone/device panels, and add bundled help SVG assets under static.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-08 10:33:41 +12:00
parent 2382ef16a1
commit aab62efd4f
27 changed files with 1606 additions and 467 deletions

View File

@@ -10,6 +10,10 @@
<div class="app-container">
<header>
<div class="header-end">
<div class="zones-menu-mobile">
<button type="button" class="btn btn-secondary" id="zones-menu-btn" aria-haspopup="true" aria-expanded="false" aria-controls="zones-menu-dropdown">Zones</button>
<div id="zones-menu-dropdown" class="zones-menu-dropdown" role="menu" aria-label="Zones"></div>
</div>
<div class="nav-slide-toggle-wrap seq-switch-toggle-wrap edit-mode-only" id="seq-switch-toggle-wrap">
<span class="nav-slide-toggle-side-label nav-slide-toggle-side-label--beat">Beat</span>
<button type="button" role="switch" class="nav-slide-toggle-switch seq-switch-toggle" id="seq-switch-toggle" aria-pressed="false" aria-label="Switch sequence on beat" title="When starting a sequence: wait for beat or downbeat">
@@ -61,7 +65,7 @@
<button type="button" data-target="profiles-btn">Profiles</button>
<button type="button" class="edit-mode-only" data-target="devices-btn">Devices</button>
<button type="button" class="edit-mode-only" data-target="groups-btn">Groups</button>
<button type="button" class="edit-mode-only" data-target="zones-btn">Tabs</button>
<button type="button" class="edit-mode-only" data-target="zones-btn">Zones</button>
<button type="button" class="edit-mode-only" data-target="presets-btn">Presets</button>
<button type="button" class="edit-mode-only" data-target="sequences-btn">Sequences</button>
<button type="button" class="edit-mode-only" data-target="patterns-btn">Patterns</button>
@@ -88,34 +92,37 @@
</div>
</div>
<!-- Tabs Modal -->
<!-- Zones Modal -->
<div id="zones-modal" class="modal">
<div class="modal-content">
<h2>Tabs</h2>
<div class="modal-head">
<h2>Zones</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="zones-close-btn">Close</button>
</div>
</div>
<div class="profiles-actions zone-modal-create-row">
<input type="text" id="new-zone-name" placeholder="Zone name">
<button class="btn btn-primary" id="create-zone-btn">Create</button>
</div>
<div class="zone-content-kind-row muted-text">
<label><input type="radio" name="new-zone-content-kind" value="presets" checked> Presets</label>
<label><input type="radio" name="new-zone-content-kind" value="sequences"> Sequences</label>
</div>
<div id="zones-list-modal" class="profiles-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="zones-close-btn">Close</button>
</div>
</div>
</div>
<!-- Edit Zone Modal -->
<div id="edit-zone-modal" class="modal">
<div class="modal-content">
<h2>Edit Zone</h2>
<div class="modal-head">
<h2>Edit Zone</h2>
<div class="modal-top-actions">
<button type="submit" form="edit-zone-form" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('edit-zone-modal').classList.remove('active')">Close</button>
</div>
</div>
<form id="edit-zone-form">
<input type="hidden" id="edit-zone-id">
<label>Zone Name:</label>
<input type="text" id="edit-zone-name" placeholder="Enter zone name" required>
<p id="edit-zone-type-label" class="zone-content-kind-row muted-text" aria-live="polite"></p>
<div id="edit-zone-block-groups">
<label class="zone-devices-label">Device groups on this zone</label>
<div id="edit-zone-groups-editor" class="zone-devices-editor"></div>
@@ -132,10 +139,6 @@
<label class="zone-presets-section-label">Add a sequence to this zone</label>
<div id="edit-zone-sequences-list" class="profiles-list edit-zone-presets-scroll"></div>
</div>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('edit-zone-modal').classList.remove('active')">Close</button>
</div>
</form>
</div>
</div>
@@ -143,7 +146,12 @@
<!-- Profiles Modal -->
<div id="profiles-modal" class="modal">
<div class="modal-content">
<h2>Profiles</h2>
<div class="modal-head">
<h2>Profiles</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="profiles-close-btn">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" id="new-profile-name" placeholder="Profile name">
<button class="btn btn-primary" id="create-profile-btn">Create</button>
@@ -156,16 +164,18 @@
</label>
</div>
<div id="profiles-list" class="profiles-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="profiles-close-btn">Close</button>
</div>
</div>
</div>
<!-- Devices Modal (registry: Wi-Fi drivers appear when they connect over TCP) -->
<div id="devices-modal" class="modal">
<div class="modal-content">
<h2>Devices</h2>
<div class="modal-head">
<h2>Devices</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" id="devices-close-btn">Close</button>
</div>
</div>
<div class="form-group" style="margin-bottom:0.75rem;">
<div class="profiles-actions" style="gap:0.5rem;flex-wrap:wrap;">
<input type="text" id="devices-add-name" placeholder="Device name" autocomplete="off" style="min-width:10rem;">
@@ -186,7 +196,6 @@
<span id="devices-ping-status" class="muted-text" aria-live="polite"></span>
<button type="button" class="btn btn-secondary" id="devices-update-groups-btn" title="Push group membership from Device groups to all ESP-NOW drivers">Update groups</button>
<span id="devices-groups-status" class="muted-text" aria-live="polite"></span>
<button type="button" class="btn btn-secondary" id="devices-close-btn">Close</button>
</div>
</div>
</div>
@@ -194,7 +203,12 @@
<!-- Device groups: members + WiFi driver defaults (zones reference groups for presets) -->
<div id="groups-modal" class="modal">
<div class="modal-content">
<h2>Device groups</h2>
<div class="modal-head">
<h2>Device groups</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="groups-close-btn">Close</button>
</div>
</div>
<p class="muted-text" style="margin-top:0;">Assign drivers to a group, set WiFi defaults once per group, then attach groups to a zone for standalone presets (sequences use each lanes groups only). By default new groups are <strong>shared</strong> across all profiles; tick “this profile only” to hide a group from other profiles.</p>
<div class="profiles-actions zone-modal-create-row">
<input type="text" id="new-group-name" placeholder="Group name">
@@ -204,15 +218,18 @@
<button class="btn btn-primary" id="create-group-btn">Create</button>
</div>
<div id="groups-list-modal" class="profiles-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="groups-close-btn">Close</button>
</div>
</div>
</div>
<div id="edit-group-modal" class="modal">
<div class="modal-content">
<h2>Edit device group</h2>
<div class="modal-head">
<h2>Edit device group</h2>
<div class="modal-top-actions">
<button type="submit" form="edit-group-form" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-group-close-btn">Close</button>
</div>
</div>
<form id="edit-group-form">
<input type="hidden" id="edit-group-id">
<label for="edit-group-name">Group name</label>
@@ -232,40 +249,19 @@
<input type="range" id="edit-group-output-brightness" min="0" max="255" value="255" style="flex:1;">
<span id="edit-group-output-brightness-value" class="muted-text" style="min-width:2.5rem;">255</span>
</div>
<p class="muted-text" style="margin-top:0.75rem;margin-bottom:0.35rem;">WiFi driver defaults (apply to all members via <strong>Apply defaults to drivers</strong> on the list)</p>
<label for="edit-group-wifi-driver-name">Display name</label>
<input type="text" id="edit-group-wifi-driver-name" placeholder="hello / discovery name" autocomplete="off">
<label for="edit-group-wifi-num-leds" style="margin-top:0.5rem;display:block;">Number of LEDs</label>
<input type="number" id="edit-group-wifi-num-leds" min="1" max="2048" step="1" placeholder="119">
<label for="edit-group-wifi-color-order" style="margin-top:0.5rem;display:block;">Colour order</label>
<select id="edit-group-wifi-color-order">
<option value="rgb">RGB</option>
<option value="rbg">RBG</option>
<option value="grb">GRB</option>
<option value="gbr">GBR</option>
<option value="brg">BRG</option>
<option value="bgr">BGR</option>
</select>
<label for="edit-group-wifi-startup-mode" style="margin-top:0.5rem;display:block;">Power-on pattern</label>
<select id="edit-group-wifi-startup-mode">
<option value="default">Default preset</option>
<option value="last">Last preset</option>
<option value="off">Off</option>
</select>
<label for="edit-group-debug" style="margin-top:1rem;display:block;">Debug</label>
<small class="muted-text" style="display:block;margin-bottom:0.35rem;">Stored row and the JSON preview for <strong>Save</strong> (updates as you edit).</small>
<textarea id="edit-group-debug" rows="8" readonly spellcheck="false" style="width:100%;font-family:monospace;resize:vertical;"></textarea>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-group-close-btn">Cancel</button>
</div>
</form>
</div>
</div>
<div id="edit-device-modal" class="modal">
<div class="modal-content">
<h2>Edit device</h2>
<div class="modal-head">
<h2>Edit device</h2>
<div class="modal-top-actions">
<button type="submit" form="edit-device-form" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-device-close-btn">Close</button>
</div>
</div>
<form id="edit-device-form">
<input type="hidden" id="edit-device-id">
<p class="muted-text" style="margin-bottom:0.75rem;">MAC (id): <code id="edit-device-storage-id"></code></p>
@@ -319,10 +315,6 @@
<label for="edit-device-debug" style="margin-top:1rem;display:block;">Debug</label>
<small class="muted-text" style="display:block;margin-bottom:0.35rem;">Stored registry row and the JSON preview for <strong>Save</strong> (updates as you edit).</small>
<textarea id="edit-device-debug" rows="8" readonly spellcheck="false" style="width:100%;font-family:monospace;resize:vertical;"></textarea>
<div class="modal-actions">
<button type="submit" class="btn btn-primary">Save</button>
<button type="button" class="btn btn-secondary" id="edit-device-close-btn">Cancel</button>
</div>
</form>
</div>
</div>
@@ -330,39 +322,48 @@
<!-- Presets Modal -->
<div id="presets-modal" class="modal">
<div class="modal-content">
<h2>Presets</h2>
<div class="modal-head">
<h2>Presets</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="presets-close-btn">Close</button>
</div>
</div>
<div class="modal-actions">
<button class="btn btn-primary" id="preset-add-btn">Add</button>
<button type="button" class="btn btn-secondary" id="import-preset-btn">Import</button>
<button class="btn btn-danger" id="preset-clear-device-btn">Clear Device Presets</button>
</div>
<div id="presets-list" class="profiles-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="presets-close-btn">Close</button>
</div>
</div>
</div>
<!-- Sequences Modal -->
<div id="sequences-modal" class="modal">
<div class="modal-content">
<h2>Sequences</h2>
<div class="modal-head">
<h2>Sequences</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" id="sequences-close-btn">Close</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="sequence-add-btn">Add</button>
<button type="button" class="btn btn-secondary" id="import-sequence-btn">Import</button>
<button type="button" class="btn btn-secondary" id="sequences-open-presets-btn">Presets</button>
</div>
<div id="sequences-list" class="profiles-list"></div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="sequences-close-btn">Close</button>
</div>
</div>
</div>
<!-- Sequence Editor Modal -->
<div id="sequence-editor-modal" class="modal">
<div class="modal-content">
<h2>Sequence</h2>
<div class="modal-head">
<h2>Sequence</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" id="sequence-editor-save-btn">Save</button>
<button type="button" class="btn btn-secondary" id="sequence-editor-close-btn">Close</button>
</div>
</div>
<div class="preset-editor-field">
<label for="sequence-editor-name">Name</label>
<input type="text" id="sequence-editor-name" placeholder="Sequence name" style="width:100%;max-width:24rem;">
@@ -385,8 +386,6 @@
<div class="modal-actions preset-editor-modal-actions">
<button type="button" class="btn btn-secondary btn-small" id="sequence-editor-add-lane-btn">Add lane</button>
<button type="button" class="btn btn-danger" id="sequence-editor-delete-btn">Delete</button>
<button type="button" class="btn btn-primary" id="sequence-editor-save-btn">Save</button>
<button type="button" class="btn btn-secondary" id="sequence-editor-close-btn">Close</button>
</div>
</div>
</div>
@@ -394,7 +393,13 @@
<!-- Preset Editor Modal -->
<div id="preset-editor-modal" class="modal">
<div class="modal-content">
<h2>Preset</h2>
<div class="modal-head">
<h2>Preset</h2>
<div class="modal-top-actions">
<button class="btn btn-primary" id="preset-save-btn">Save</button>
<button class="btn btn-secondary" id="preset-editor-close-btn">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" id="preset-name-input" placeholder="Preset name">
<select id="preset-pattern-input">
@@ -485,8 +490,6 @@
<button class="btn btn-secondary" id="preset-send-btn">Try</button>
<button class="btn btn-secondary" id="preset-default-btn">Default</button>
<button type="button" class="btn btn-danger" id="preset-remove-from-zone-btn" hidden>Remove from zone</button>
<button class="btn btn-primary" id="preset-save-btn">Save &amp; Send</button>
<button class="btn btn-secondary" id="preset-editor-close-btn">Close</button>
</div>
</div>
</div>
@@ -494,22 +497,30 @@
<!-- Patterns Modal -->
<div id="patterns-modal" class="modal">
<div class="modal-content">
<h2>Patterns</h2>
<div class="modal-head">
<h2>Patterns</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="patterns-close-btn">Close</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" id="pattern-add-btn">Add</button>
<button type="button" class="btn btn-secondary" id="pattern-send-all-btn">Send All Patterns</button>
</div>
<div id="patterns-list" class="profiles-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" id="patterns-close-btn">Close</button>
</div>
</div>
</div>
<!-- Pattern Editor Modal -->
<div id="pattern-editor-modal" class="modal">
<div class="modal-content">
<h2>Pattern</h2>
<div class="modal-head">
<h2>Pattern</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" id="pattern-create-btn">Save</button>
<button type="button" class="btn btn-secondary" id="pattern-editor-close-btn">Close</button>
</div>
</div>
<p class="muted-text" style="margin: 0 0 0.75rem 0;">Add a driver <code>.py</code> file and editor metadata (stored in the pattern database).</p>
<div class="profiles-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
<label for="pattern-create-name" style="min-width: 7rem;">Name</label>
@@ -572,8 +583,6 @@
<input type="checkbox" id="pattern-create-overwrite" checked>
<span>Overwrite existing file</span>
</label>
<button type="button" class="btn btn-primary" id="pattern-create-btn">Save</button>
<button type="button" class="btn btn-secondary" id="pattern-editor-close-btn">Close</button>
</div>
</div>
</div>
@@ -581,63 +590,522 @@
<!-- Colour Palette Modal -->
<div id="color-palette-modal" class="modal">
<div class="modal-content">
<h2>Colour Palette</h2>
<div class="modal-head">
<h2>Colour Palette</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="color-palette-close-btn">Close</button>
</div>
</div>
<p class="muted-text">Profile: <span id="palette-current-profile-name">None</span></p>
<div id="palette-container" class="profiles-list"></div>
<div class="profiles-actions">
<input type="color" id="palette-new-color" value="#ffffff">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" id="color-palette-close-btn">Close</button>
</div>
</div>
</div>
<!-- Help Modal -->
<div id="help-modal" class="modal">
<div class="modal-content">
<h2>Help</h2>
<p class="muted-text">How to use the LED controller UI.</p>
<h3>Run mode</h3>
<ul>
<li><strong>Select zone</strong>: left-click a zone button in the top bar.</li>
<li><strong>Select preset</strong>: left-click a preset tile to send a <code>select</code> message to all devices in the zone.</li>
<li><strong>Profiles</strong>: open <strong>Profiles</strong> to apply a profile. Profile editing actions are hidden in Run mode.</li>
<li><strong>Devices</strong>: open <strong>Devices</strong> to see drivers (Wi-Fi clients appear when they connect); edit addresses or remove rows.</li>
<li><strong>Groups</strong>: define device groups, WiFi driver defaults, then assign groups to zones.</li>
<li><strong>Send all presets</strong>: this action is available in <strong>Edit mode</strong> and pushes every preset used in the current zone to all zone devices.</li>
<li><strong>Switch modes</strong>: use the mode button in the menu. The button label shows the mode you will switch to.</li>
</ul>
<h3>Edit mode</h3>
<ul>
<li><strong>Tabs</strong>: create, edit, and manage zones and which <strong>device groups</strong> each zone drives.</li>
<li><strong>Presets</strong>: create/manage reusable presets and edit preset details.</li>
<li><strong>Preset tiles</strong>: each tile shows <strong>Edit</strong> and <strong>Remove</strong> controls in Edit mode.</li>
<li><strong>Reorder presets</strong>: drag and drop preset tiles to save zone order.</li>
<li><strong>Profiles</strong>: create/clone/delete profiles. New profiles get a populated <strong>default</strong> zone and can optionally seed a <strong>DJ zone</strong>.</li>
<li><strong>Devices</strong>: registry rows are keyed by <strong>MAC</strong>; edit a device for transport/IP and per-driver WiFi settings, or use <strong>Groups</strong> for shared defaults.</li>
<li><strong>Colour Palette</strong>: build profile colours and use <strong>From Palette</strong> in preset editor to add linked colours (badge <strong>P</strong>) that update when palette colours change.</li>
</ul>
<h3>LED Tool (Settings tab)</h3>
<ul>
<li><strong>USB device setup</strong>: updates <code>settings.json</code> on ESP32 drivers over serial (for example name, pin, LED count, Wi-Fi credentials).</li>
<li><strong>Deploy and maintenance</strong>: uploads driver files, flashes firmware, resets device, and follows serial logs.</li>
<li><strong>Scope</strong>: led-tool configures devices directly; this web UI controls profiles/zones/presets and sends runtime messages. Open <strong>Settings → LED Tool</strong> in Edit mode.</li>
</ul>
<div class="modal-actions">
<button class="btn btn-secondary" id="help-close-btn">Close</button>
<div class="modal-content help-modal-content">
<div class="modal-head">
<h2>Help</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="help-close-btn">Close</button>
</div>
</div>
<p class="muted-text help-modal-intro">How to use the LED controller UI. Previews use the same styles as the live interface.</p>
<div class="help-tabs" role="tablist" aria-label="Help sections">
<button type="button" class="help-tab-btn active" role="tab" id="help-tab-overview" data-help-tab="overview" aria-selected="true" aria-controls="help-panel-overview">Overview</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-profiles" data-help-tab="profiles" aria-selected="false" aria-controls="help-panel-profiles">Profiles</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-devices" data-help-tab="devices" aria-selected="false" aria-controls="help-panel-devices">Devices</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-groups" data-help-tab="groups" aria-selected="false" aria-controls="help-panel-groups">Groups</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-zones" data-help-tab="zones" aria-selected="false" aria-controls="help-panel-zones">Zones</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-presets" data-help-tab="presets" aria-selected="false" aria-controls="help-panel-presets">Presets</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-sequences" data-help-tab="sequences" aria-selected="false" aria-controls="help-panel-sequences">Sequences</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-patterns" data-help-tab="patterns" aria-selected="false" aria-controls="help-panel-patterns">Patterns</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-colour-palette" data-help-tab="colour-palette" aria-selected="false" aria-controls="help-panel-colour-palette">Colour Palette</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-audio" data-help-tab="audio" aria-selected="false" aria-controls="help-panel-audio">Audio</button>
<button type="button" class="help-tab-btn" role="tab" id="help-tab-settings" data-help-tab="settings" aria-selected="false" aria-controls="help-panel-settings">Settings</button>
</div>
<div id="help-panel-overview" class="help-tab-panel active" data-help-panel="overview" role="tabpanel" aria-labelledby="help-tab-overview">
<div class="help-ui-preview help-ui-preview--header" aria-hidden="true">
<div class="help-preview-header">
<div class="header-end">
<div class="header-actions">
<div class="header-brightness-control">
<label>Brightness</label>
<input type="range" min="0" max="255" value="200" tabindex="-1">
</div>
<button type="button" class="btn btn-secondary" tabindex="-1">Profiles</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Devices</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Zones</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Presets</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Audio</button>
<button type="button" class="btn btn-secondary ui-mode-toggle" tabindex="-1">Run mode</button>
</div>
</div>
<div class="zones-container">
<div class="zones-list">
<button type="button" class="zone-button" tabindex="-1">default</button>
<button type="button" class="zone-button active" tabindex="-1">lounge</button>
<button type="button" class="zone-button" tabindex="-1">dj</button>
</div>
</div>
</div>
</div>
<p class="help-ui-preview-caption">Zone buttons below the header; management buttons on the right (Edit mode).</p>
<h3>Run mode and Edit mode</h3>
<ul>
<li><strong>Run mode</strong>: day-to-day control — choose a zone, tap presets, apply profiles. Management buttons are hidden.</li>
<li><strong>Edit mode</strong>: full setup — zones, presets, sequences, patterns, colour palette, and per-tile <strong>Edit</strong> on the strip.</li>
<li><strong>Switch modes</strong>: use the mode button in the header or mobile menu. The label shows the mode you will switch <em>to</em>.</li>
</ul>
<div class="help-ui-preview help-ui-preview--strip" aria-hidden="true">
<div class="zone-content">
<div class="presets-section">
<div class="help-preview-presets-grid">
<div class="preset-tile-row preset-tile-row--edit">
<div class="preset-tile-row-top">
<button type="button" class="pattern-button preset-tile-main active" style="background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)),linear-gradient(to right,#ffd54f 0%,#fff8e1 100%)" tabindex="-1"><span class="pattern-button-label">warm white</span></button>
<div class="preset-tile-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
</div>
<div class="preset-tile-row preset-tile-row--edit">
<div class="preset-tile-row-top">
<button type="button" class="pattern-button preset-tile-main" style="background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)),linear-gradient(to right,#e53935 0%,#1e88e5 100%)" tabindex="-1"><span class="pattern-button-label">rainbow</span></button>
<div class="preset-tile-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
</div>
<div class="preset-tile-row preset-tile-row--edit">
<div class="preset-tile-row-top">
<button type="button" class="pattern-button preset-tile-main" style="background-image:linear-gradient(rgba(0,0,0,0.4),rgba(0,0,0,0.4)),linear-gradient(to right,#00897b 0%,#4db6ac 100%)" tabindex="-1"><span class="pattern-button-label">pulse</span></button>
<div class="preset-tile-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<p class="help-ui-preview-caption">Click a preset tile to select it on all devices in the zone.</p>
<ul>
<li><strong>Select zone</strong>: click a zone button in the top bar.</li>
<li><strong>Brightness</strong>: the header slider adjusts global brightness for the current zone.</li>
<li><strong>Edit mode</strong>: drag preset tiles to reorder; use <strong>Edit</strong> and <strong>Remove</strong> on each tile.</li>
</ul>
<div class="help-ui-preview help-ui-preview--mobile" aria-hidden="true">
<div class="help-preview-mobile-bar">
<button type="button" class="btn btn-secondary" tabindex="-1">Menu</button>
</div>
<div class="main-menu-dropdown">
<button type="button" tabindex="-1">Run mode</button>
<button type="button" tabindex="-1">Profiles</button>
<button type="button" tabindex="-1">Zones</button>
<button type="button" tabindex="-1">Presets</button>
<button type="button" tabindex="-1">Help</button>
</div>
</div>
<p class="help-ui-preview-caption">On narrow screens, <strong>Menu</strong> reaches the same actions as the desktop header.</p>
</div>
<div id="help-panel-profiles" class="help-tab-panel" data-help-panel="profiles" role="tabpanel" aria-labelledby="help-tab-profiles" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Profiles</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" value="New profile" readonly tabindex="-1">
<button type="button" class="btn btn-primary" tabindex="-1">Create</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Import</button>
</div>
<div class="profiles-list">
<div class="profiles-row">
<span style="font-weight:bold;color:#FFD700">&#10003; House default</span>
<span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Export</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Clone</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Delete</button>
</span>
</div>
<div class="profiles-row">
<span>Garden party</span>
<span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Export</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Clone</button>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Delete</button>
</span>
</div>
</div>
</div>
</div>
<ul>
<li><strong>Apply</strong>: sets the current profile. Zones and presets you see are scoped to that profile.</li>
<li><strong>Create</strong> (Edit mode): new profiles get a populated <strong>default</strong> zone. Optionally tick <strong>DJ zone</strong> for a starter <code>dj</code> zone.</li>
<li><strong>Clone</strong> / <strong>Delete</strong>: available in Edit mode from the profile list.</li>
<li>In Run mode you can only apply profiles; create, clone, and delete are hidden.</li>
</ul>
</div>
<div id="help-panel-devices" class="help-tab-panel" data-help-panel="devices" role="tabpanel" aria-labelledby="help-tab-devices" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Devices</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-list">
<div class="profiles-row">
<span class="device-status-dot device-status-dot--online" role="img"></span>
<span style="flex:1">lounge strip</span>
<code class="device-row-mac">AA:BB:CC:DD:EE:01</code>
<span class="muted-text" style="font-size:0.85em">led · espnow · —</span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Identify</button>
</div>
<div class="profiles-row">
<span class="device-status-dot device-status-dot--unknown" role="img"></span>
<span style="flex:1">ceiling</span>
<code class="device-row-mac">AA:BB:CC:DD:EE:02</code>
<span class="muted-text" style="font-size:0.85em">led · espnow · —</span>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Identify</button>
</div>
</div>
<div class="modal-actions" style="justify-content:flex-start;margin-top:0.75rem;">
<button type="button" class="btn btn-secondary" tabindex="-1">Ping drivers</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Update groups</button>
</div>
</div>
</div>
<ul>
<li><strong>Devices</strong> (Edit mode): registry of LED drivers keyed by <strong>MAC</strong>.</li>
<li>ESP-NOW devices appear automatically after <strong>ANNOUNCE</strong>; you can also add rows manually.</li>
<li><strong>Identify</strong>: short red blink (~2 s) so you can spot hardware.</li>
<li><strong>Update groups</strong>: pushes group membership from device groups to ESP-NOW drivers.</li>
<li>Edit a device for transport, IP, and per-driver settings; use <strong>Groups</strong> for shared WiFi defaults.</li>
</ul>
</div>
<div id="help-panel-groups" class="help-tab-panel" data-help-panel="groups" role="tabpanel" aria-labelledby="help-tab-groups" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Device groups</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-actions zone-modal-create-row">
<input type="text" value="Group name" readonly tabindex="-1">
<button type="button" class="btn btn-primary" tabindex="-1">Create</button>
</div>
<div class="profiles-list">
<div class="group-list-row">
<div class="group-list-row-info">
<div class="group-list-row-title">lounge lights (3 devices)</div>
<div class="group-list-row-meta muted-text">Shared across profiles</div>
</div>
<div class="group-list-row-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply brightness</button>
</div>
</div>
<div class="group-list-row">
<div class="group-list-row-info">
<div class="group-list-row-title">dj booth (2 devices)</div>
<div class="group-list-row-meta muted-text">This profile only</div>
</div>
<div class="group-list-row-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Apply brightness</button>
</div>
</div>
</div>
</div>
</div>
<ul>
<li>Assign drivers to a <strong>group</strong>, set WiFi defaults once per group, then attach groups to a zone.</li>
<li>Standalone presets use the zones device groups. Sequence lanes each target their own group.</li>
<li>New groups are <strong>shared</strong> across profiles by default; tick <strong>this profile only</strong> to hide a group elsewhere.</li>
<li>In the group editor, search and pick devices from the list to add members; <strong>Identify devices in group</strong> blinks them together.</li>
</ul>
</div>
<div id="help-panel-zones" class="help-tab-panel" data-help-panel="zones" role="tabpanel" aria-labelledby="help-tab-zones" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Edit Zone</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Save</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<label>Zone Name:</label>
<input type="text" value="lounge" readonly tabindex="-1">
<label class="zone-devices-label">Device groups on this zone</label>
<div class="profiles-list">
<div class="profiles-row edit-zone-item-row"><span>lounge lights</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
</div>
<label class="zone-presets-section-label">Presets on this zone</label>
<div class="profiles-list edit-zone-presets-scroll">
<div class="profiles-row edit-zone-item-row"><span>warm white</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
<div class="profiles-row edit-zone-item-row"><span>rainbow</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
</div>
<label class="zone-presets-section-label">Sequences on this zone</label>
<div class="profiles-list edit-zone-presets-scroll">
<div class="profiles-row edit-zone-item-row"><span>intro build</span><button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button></div>
</div>
</div>
</div>
<ul>
<li><strong>Zones</strong> (Edit mode): create and manage zones from the header <strong>Zones</strong> button.</li>
<li>Each zone lists <strong>device groups</strong>, <strong>presets</strong>, and <strong>sequences</strong> — presets and sequences can share the same zone.</li>
<li>Drag presets on the main strip or in the zone editor to reorder.</li>
<li>Right-click a zone button for quick access to zone settings.</li>
</ul>
</div>
<div id="help-panel-presets" class="help-tab-panel" data-help-panel="presets" role="tabpanel" aria-labelledby="help-tab-presets" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Preset</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Save</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="profiles-actions">
<input type="text" value="evening glow" readonly tabindex="-1">
<select tabindex="-1"><option>pulse</option></select>
</div>
<label>Colours</label>
<div class="preset-colors-container">
<div class="help-preview-color-swatch" style="background-color:#7e57c2"><span class="help-preview-p-badge">P</span></div>
<div class="help-preview-color-swatch" style="background-color:#26a69a"></div>
</div>
<div class="profiles-actions">
<input type="color" value="#ffffff" tabindex="-1">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">From Palette</button>
</div>
<div class="modal-actions preset-editor-modal-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Try</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Default</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Send</button>
</div>
</div>
</div>
<ul>
<li><strong>Presets</strong> (Edit mode): profile-wide list — <strong>Add</strong>, <strong>Edit</strong>, <strong>Send</strong>, and <strong>Delete</strong>.</li>
<li><strong>Pattern</strong> and optional <strong>n1n8</strong> fields depend on the pattern.</li>
<li><strong>From Palette</strong>: inserts a colour linked to the profile palette (badge <strong>P</strong>).</li>
<li><strong>Try</strong>: previews on the current zone without saving on the device.</li>
<li><strong>Save</strong>: writes the preset to the server (does not close the editor).</li>
<li><strong>Send</strong>: pushes the definition to devices with save.</li>
<li><strong>Remove from zone</strong> (when opened from a zone): removes from this zone only.</li>
</ul>
</div>
<div id="help-panel-sequences" class="help-tab-panel" data-help-panel="sequences" role="tabpanel" aria-labelledby="help-tab-sequences" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Sequence</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Save</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="preset-editor-field">
<label>Name</label>
<input type="text" value="intro build" readonly tabindex="-1">
</div>
<p class="muted-text" style="font-size:0.85em;margin:0.5rem 0;">Lane 1 — lounge lights</p>
<div class="profiles-list">
<div class="sequence-step-row profiles-row" style="display:flex;flex-direction:column;gap:0.35rem;padding:0.5rem;border:1px solid rgba(255,255,255,0.12);border-radius:6px;">
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;">
<label>Preset</label>
<select tabindex="-1"><option>warm white — 1</option></select>
<label>Beats</label>
<input type="number" value="4" readonly style="width:4rem" tabindex="-1">
</div>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Add lane</button>
</div>
</div>
</div>
<ul>
<li><strong>Sequences</strong> (Edit mode): build multi-step shows with one or more <strong>lanes</strong> (each lane targets a device group).</li>
<li>Add presets as steps per lane; open from the zone editor to attach a sequence to a zone.</li>
<li><strong>Beat</strong> / <strong>Downbeat</strong> toggle (header): when starting a sequence, wait for beat or downbeat before step 1.</li>
<li>Tap <kbd>S</kbd> or the BPM button during playback to sync step timing to music (with Audio running).</li>
</ul>
</div>
<div id="help-panel-patterns" class="help-tab-panel" data-help-panel="patterns" role="tabpanel" aria-labelledby="help-tab-patterns" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Patterns</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="modal-actions" style="margin-top:0;justify-content:flex-start;">
<button type="button" class="btn btn-primary" tabindex="-1">Add</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Send All Patterns</button>
</div>
<div class="profiles-list">
<div class="profiles-row"><span>pulse</span><span class="muted-text">delay 20200 ms</span><button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button></div>
<div class="profiles-row"><span>rainbow</span><span class="muted-text">delay 1080 ms</span><button type="button" class="btn btn-secondary btn-small" tabindex="-1">Edit</button></div>
</div>
</div>
</div>
<ul>
<li><strong>Patterns</strong> (Edit mode): reference list of pattern names and typical delay ranges.</li>
<li>Choose the pattern inside the preset editor; parameters map to <strong>n1n8</strong>.</li>
<li>WiFi drivers can install pattern modules over HTTP (OTA upload); ESP-NOW devices use the bridge you configure in <strong>Settings</strong>.</li>
</ul>
</div>
<div id="help-panel-colour-palette" class="help-tab-panel" data-help-panel="colour-palette" role="tabpanel" aria-labelledby="help-tab-colour-palette" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content help-preview-surface">
<div class="modal-head">
<h2>Colour Palette</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<p class="muted-text">Profile: <span>House default</span></p>
<div id="palette-container" class="profiles-list">
<div class="profiles-row" style="display:flex;align-items:center;gap:1rem;">
<div style="width:64px;height:64px;border-radius:8px;background:#7e57c2;border:2px solid #4a4a4a;flex-shrink:0;"></div>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
<div class="profiles-row" style="display:flex;align-items:center;gap:1rem;">
<div style="width:64px;height:64px;border-radius:8px;background:#26a69a;border:2px solid #4a4a4a;flex-shrink:0;"></div>
<button type="button" class="btn btn-danger btn-small" tabindex="-1">Remove</button>
</div>
</div>
<div class="profiles-actions">
<input type="color" value="#ffffff" tabindex="-1">
</div>
</div>
</div>
<p class="help-ui-preview-caption">Add or change swatches; linked preset colours update automatically.</p>
<ul>
<li><strong>Colour Palette</strong> (Edit mode): edits the current profiles palette swatches.</li>
<li>Use <strong>From Palette</strong> in the preset editor for colours that stay in sync (badge <strong>P</strong>).</li>
</ul>
</div>
<div id="help-panel-audio" class="help-tab-panel" data-help-panel="audio" role="tabpanel" aria-labelledby="help-tab-audio" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content audio-modal-content help-preview-surface">
<div class="modal-head">
<h2>Audio Beat Detection</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="form-group audio-device-block">
<label>Input device</label>
<div class="profiles-actions audio-device-select-row">
<select tabindex="-1"><option>Monitor of Built-in Audio</option></select>
<button type="button" class="btn btn-secondary btn-small" tabindex="-1">Refresh</button>
</div>
</div>
<div class="form-group">
<label>Beat indicators</label>
<button type="button" class="audio-beat-sync-btn audio-modal-beat-sync" tabindex="-1">
<span class="audio-top-indicator-label">BPM</span>
<span class="audio-top-indicator-value">128</span>
</button>
</div>
<div class="form-group audio-volume-block">
<div class="audio-volume-header">
<label>Volume</label>
<span class="audio-volume-readout">100% (0.00 dB)</span>
</div>
<input type="range" class="audio-volume-slider" min="0" max="200" value="100" tabindex="-1">
</div>
<div class="modal-actions">
<button type="button" class="btn btn-primary" tabindex="-1">Start</button>
<button type="button" class="btn btn-secondary" tabindex="-1">Stop</button>
</div>
</div>
</div>
<ul>
<li><strong>Audio</strong>: beat detection from a chosen input device (monitor sources follow playback).</li>
<li>BPM and beat indicators appear in the header and Audio modal while detection is running.</li>
<li>Adjust <strong>Volume</strong> (gain before detection); the level meter shows live input.</li>
<li><strong>Start</strong> / <strong>Stop</strong> detection; <strong>Reset detector</strong> clears stuck BPM tracking.</li>
<li>Sync sequences to music with <kbd>S</kbd> on a downbeat while a sequence plays.</li>
</ul>
</div>
<div id="help-panel-settings" class="help-tab-panel" data-help-panel="settings" role="tabpanel" aria-labelledby="help-tab-settings" hidden>
<div class="help-ui-preview" aria-hidden="true">
<div class="modal-content settings-modal-content help-preview-surface">
<div class="modal-head">
<h2>Settings</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" tabindex="-1">Close</button>
</div>
</div>
<div class="settings-tabs" role="tablist">
<button type="button" class="settings-tab-btn active" tabindex="-1">Bridge</button>
<button type="button" class="settings-tab-btn" tabindex="-1">LED Tool</button>
</div>
<div class="settings-section">
<span class="muted-text">USB serial: /dev/ttyUSB0 (connected)</span>
<h3 class="settings-subheading" style="margin-top:0.75rem;">Wi-Fi</h3>
<p class="muted-text" style="margin:0;">Bridge-AP — ws://192.168.4.1/ws</p>
</div>
</div>
</div>
<ul>
<li><strong>Settings</strong> (Edit mode): <strong>Bridge</strong> connects the Pi to ESP-NOW hardware over USB serial or WiFi.</li>
<li>Save bridge profiles, scan for the bridge AP, and check connection status.</li>
<li><strong>LED Tool</strong>: USB serial setup for drivers — <code>settings.json</code>, deploy, flash, and maintenance.</li>
<li>LED Tool configures devices directly; this UI controls profiles, zones, presets, and runtime messages.</li>
</ul>
</div>
</div>
</div>
<!-- Audio Modal -->
<div id="audio-modal" class="modal">
<div class="modal-content audio-modal-content">
<h2>Audio Beat Detection</h2>
<div class="modal-head">
<h2>Audio Beat Detection</h2>
<div class="modal-top-actions">
<button type="button" class="btn btn-secondary" id="audio-close-btn">Close</button>
</div>
</div>
<div class="form-group audio-device-block">
<label for="audio-device-select">Input device</label>
<div class="profiles-actions audio-device-select-row">
@@ -683,7 +1151,6 @@
<button type="button" class="btn btn-primary" id="audio-start-btn">Start</button>
<button type="button" class="btn btn-secondary" id="audio-stop-btn">Stop</button>
<button type="button" class="btn btn-secondary" id="audio-reset-btn" disabled title="Clear stuck BPM / beat tracking">Reset detector</button>
<button type="button" class="btn btn-secondary" id="audio-close-btn">Close</button>
</div>
</div>
</div>
@@ -691,7 +1158,12 @@
<!-- Settings Modal -->
<div id="settings-modal" class="modal">
<div class="modal-content settings-modal-content">
<h2>Settings</h2>
<div class="modal-head">
<h2>Settings</h2>
<div class="modal-top-actions">
<button class="btn btn-secondary" id="settings-close-btn">Close</button>
</div>
</div>
<div class="settings-tabs" role="tablist" aria-label="Settings sections">
<button type="button" class="settings-tab-btn active" role="tab" id="settings-tab-bridge" data-settings-tab="bridge" aria-selected="true" aria-controls="settings-panel-bridge">Bridge</button>
<button type="button" class="settings-tab-btn" role="tab" id="settings-tab-led-tool" data-settings-tab="led-tool" aria-selected="false" aria-controls="settings-panel-led-tool">LED Tool</button>
@@ -778,9 +1250,6 @@
<iframe id="led-tool-iframe" title="LED device settings editor" src="about:blank" allow="serial" class="settings-led-tool-iframe"></iframe>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" id="settings-close-btn">Close</button>
</div>
</div>
</div>