diff --git a/docs/images/help/header-toolbar.svg b/docs/images/help/header-toolbar.svg index c727a74..5187199 100644 --- a/docs/images/help/header-toolbar.svg +++ b/docs/images/help/header-toolbar.svg @@ -2,7 +2,7 @@ Header: tab buttons and action bar - Tabs + Zones default @@ -13,7 +13,7 @@ Profiles - Tabs + Zones Presets diff --git a/docs/images/help/mobile-menu.svg b/docs/images/help/mobile-menu.svg index 6f63cfa..5976b83 100644 --- a/docs/images/help/mobile-menu.svg +++ b/docs/images/help/mobile-menu.svg @@ -1,26 +1,26 @@ - - Narrow screen: Menu aggregates header actions + + Narrow screen: Menu aggregates header actions - Menu � + Menu - tab + tab - tab + tab - Dropdown (same actions as desktop header) - + Dropdown (same actions as desktop header) + Run mode Profiles - Tabs + Zones Presets Help - Content area  presets as on desktop + Content area - presets as on desktop - preset + preset - preset + preset diff --git a/src/static/app.js b/src/static/app.js index 4af1d43..3bc4de8 100644 --- a/src/static/app.js +++ b/src/static/app.js @@ -872,7 +872,7 @@ class LightingController { this.selectTab(this.state.zone_order[0]); } else { this.currentTab = null; - document.getElementById('zone-content').innerHTML = '

No tabs available. Create a new zone to get started.

'; + document.getElementById('zone-content').innerHTML = '

No zones available. Create a new zone to get started.

'; } } } catch (error) { @@ -1010,7 +1010,7 @@ class LightingController { this.state.lights = {}; this.state.zone_order = []; this.renderTabs(); - document.getElementById('zone-content').innerHTML = '

No tabs available. Create a new zone to get started.

'; + document.getElementById('zone-content').innerHTML = '

No zones available. Create a new zone to get started.

'; this.updateCurrentProfileDisplay(); } } else { diff --git a/src/static/devices.js b/src/static/devices.js index 5f7541b..5767704 100644 --- a/src/static/devices.js +++ b/src/static/devices.js @@ -938,7 +938,8 @@ document.addEventListener('DOMContentLoaded', () => { }); if (!pushRes.ok) return; } - editDeviceModal.classList.remove('active'); + await loadDevicesModal(); + refreshEditDeviceDebug(); }); } if (editCloseBtn) { diff --git a/src/static/groups.js b/src/static/groups.js index 4226eaa..bd28b31 100644 --- a/src/static/groups.js +++ b/src/static/groups.js @@ -85,29 +85,33 @@ function renderGroupDevicesEditor(containerEl, macRows, devicesMap) { const macsInRows = new Set(macRows.map((r) => r.mac).filter(Boolean)); const addWrap = document.createElement('div'); addWrap.className = 'zone-devices-add profiles-actions'; - const sel = document.createElement('select'); - sel.className = 'zone-device-add-select'; - sel.appendChild(new Option('Add device…', '')); - entries.forEach(([mac, d]) => { - if (macsInRows.has(mac)) return; - const labelName = d && d.name ? String(d.name).trim() : ''; - const optLabel = labelName ? `${labelName} — ${mac}` : mac; - sel.appendChild(new Option(optLabel, mac)); - }); - const addBtn = document.createElement('button'); - addBtn.type = 'button'; - addBtn.className = 'btn btn-primary btn-small'; - addBtn.textContent = 'Add'; - addBtn.addEventListener('click', () => { - const mac = sel.value; - if (!mac || !devicesMap[mac]) return; - const n = String((devicesMap[mac].name || '').trim() || mac); - macRows.push({ mac, label: n }); - sel.value = ''; - renderGroupDevicesEditor(containerEl, macRows, devicesMap); - }); - addWrap.appendChild(sel); - addWrap.appendChild(addBtn); + const picker = + typeof window.createSearchableAddPicker === 'function' + ? window.createSearchableAddPicker({ + entries, + excludeIds: macsInRows, + labelFor: (mac, d) => { + const labelName = d && d.name ? String(d.name).trim() : ''; + return labelName ? `${labelName} — ${mac}` : mac; + }, + searchTextFor: (mac, d) => { + const labelName = d && d.name ? String(d.name).trim() : ''; + return `${labelName} ${mac}`; + }, + onPick: (mac, d) => { + if (!mac || !devicesMap[mac]) return; + const n = String((d.name || '').trim() || mac); + macRows.push({ mac, label: n }); + renderGroupDevicesEditor(containerEl, macRows, devicesMap); + }, + placeholder: 'Search devices to add…', + emptyMessage: 'No devices match your search.', + noItemsMessage: 'All devices are already in this group.', + }) + : null; + if (picker) { + addWrap.appendChild(picker); + } if (panel) { panel.addSlot.appendChild(addWrap); } else { @@ -130,15 +134,17 @@ function collectGroupEditPayload() { const nl = document.getElementById('edit-group-wifi-num-leds'); const co = document.getElementById('edit-group-wifi-color-order'); const ws = document.getElementById('edit-group-wifi-startup-mode'); - if (dn && dn.value.trim()) payload.wifi_driver_display_name = dn.value.trim(); - else payload.wifi_driver_display_name = null; - if (nl && nl.value !== '') { - const n = parseInt(nl.value, 10); - if (!Number.isNaN(n) && n >= 1) payload.wifi_driver_num_leds = n; - else payload.wifi_driver_num_leds = null; - } else payload.wifi_driver_num_leds = null; - if (co && co.value) payload.wifi_color_order = co.value; - if (ws && ws.value) payload.wifi_startup_mode = ws.value; + if (dn || nl || co || ws) { + if (dn && dn.value.trim()) payload.wifi_driver_display_name = dn.value.trim(); + else if (dn) payload.wifi_driver_display_name = null; + if (nl && nl.value !== '') { + const n = parseInt(nl.value, 10); + if (!Number.isNaN(n) && n >= 1) payload.wifi_driver_num_leds = n; + else payload.wifi_driver_num_leds = null; + } else if (nl) payload.wifi_driver_num_leds = null; + if (co && co.value) payload.wifi_color_order = co.value; + if (ws && ws.value) payload.wifi_startup_mode = ws.value; + } const gob = document.getElementById('edit-group-output-brightness'); if (gob && gob.value !== '') { const nb = parseInt(gob.value, 10); @@ -292,22 +298,27 @@ function renderGroupsList(groups) { ids.forEach((gid) => { const g = groups[gid]; const row = document.createElement('div'); - row.className = 'profiles-row'; - row.style.display = 'flex'; - row.style.alignItems = 'center'; - row.style.gap = '0.5rem'; - row.style.flexWrap = 'wrap'; + row.className = 'group-list-row'; - const label = document.createElement('span'); + const info = document.createElement('div'); + info.className = 'group-list-row-info'; + + const label = document.createElement('div'); + label.className = 'group-list-row-title'; const devs = Array.isArray(g.devices) ? g.devices : []; label.textContent = `${g.name || gid} (${devs.length} device${devs.length === 1 ? '' : 's'})`; const meta = document.createElement('div'); - meta.className = 'muted-text'; - meta.style.fontSize = '0.8em'; + meta.className = 'group-list-row-meta muted-text'; const rawPid = g.profile_id != null ? g.profile_id : g.profileId; const scoped = rawPid != null && String(rawPid).trim() !== ''; meta.textContent = scoped ? `This profile only (${rawPid})` : 'Shared across profiles'; + + info.appendChild(label); + info.appendChild(meta); + + const actions = document.createElement('div'); + actions.className = 'group-list-row-actions'; const editBtn = document.createElement('button'); editBtn.className = 'btn btn-secondary btn-small'; editBtn.textContent = 'Edit'; @@ -392,17 +403,13 @@ function renderGroupsList(groups) { } }); - const left = document.createElement('div'); - left.style.flex = '1'; - left.style.minWidth = '0'; - left.appendChild(label); - left.appendChild(meta); - row.appendChild(left); - row.appendChild(editBtn); - row.appendChild(brightBtn); - row.appendChild(applyBtn); - row.appendChild(identifyBtn); - row.appendChild(delBtn); + actions.appendChild(editBtn); + actions.appendChild(brightBtn); + actions.appendChild(applyBtn); + actions.appendChild(identifyBtn); + actions.appendChild(delBtn); + row.appendChild(info); + row.appendChild(actions); container.appendChild(row); }); } @@ -540,8 +547,8 @@ document.addEventListener('DOMContentLoaded', () => { } catch (_) { /* ignore push errors after save */ } - if (editModal) editModal.classList.remove('active'); await loadGroupsModal(); + refreshEditGroupDebug(); } catch (err) { console.error(err); alert('Save failed'); diff --git a/src/static/help.js b/src/static/help.js index 4719ddd..5143f04 100644 --- a/src/static/help.js +++ b/src/static/help.js @@ -7,9 +7,11 @@ document.addEventListener('DOMContentLoaded', () => { const mainMenuDropdown = document.getElementById('main-menu-dropdown'); if (helpBtn && helpModal) { - helpBtn.addEventListener('click', () => { + const openHelp = () => { helpModal.classList.add('active'); - }); + switchHelpTab('overview'); + }; + helpBtn.addEventListener('click', openHelp); } if (helpCloseBtn && helpModal) { @@ -18,10 +20,37 @@ document.addEventListener('DOMContentLoaded', () => { }); } + const helpTabButtons = document.querySelectorAll('[data-help-tab]'); + const helpTabPanels = document.querySelectorAll('[data-help-panel]'); + + function switchHelpTab(tabId) { + if (!tabId) tabId = 'overview'; + for (const btn of helpTabButtons) { + const on = btn.getAttribute('data-help-tab') === tabId; + btn.classList.toggle('active', on); + btn.setAttribute('aria-selected', on ? 'true' : 'false'); + } + for (const panel of helpTabPanels) { + const on = panel.getAttribute('data-help-panel') === tabId; + panel.classList.toggle('active', on); + panel.hidden = !on; + } + } + + for (const btn of helpTabButtons) { + btn.addEventListener('click', () => { + switchHelpTab(btn.getAttribute('data-help-tab')); + }); + } + // Mobile main menu: forward clicks to existing header buttons if (mainMenuBtn && mainMenuDropdown) { mainMenuBtn.addEventListener('click', () => { mainMenuDropdown.classList.toggle('open'); + const zonesMenuDropdown = document.getElementById('zones-menu-dropdown'); + const zonesMenuBtn = document.getElementById('zones-menu-btn'); + if (zonesMenuDropdown) zonesMenuDropdown.classList.remove('open'); + if (zonesMenuBtn) zonesMenuBtn.setAttribute('aria-expanded', 'false'); }); mainMenuDropdown.addEventListener('click', (event) => { diff --git a/src/static/images/help/audio.svg b/src/static/images/help/audio.svg new file mode 100644 index 0000000..379c22d --- /dev/null +++ b/src/static/images/help/audio.svg @@ -0,0 +1,15 @@ + + Audio beat detection + + + Audio Beat Detection + Input device + + monitor of Built-in + + BPM + 128 + + + Volume - live level meter - tap S to sync sequence + diff --git a/src/static/images/help/colour-palette.svg b/src/static/images/help/colour-palette.svg new file mode 100644 index 0000000..6f352d7 --- /dev/null +++ b/src/static/images/help/colour-palette.svg @@ -0,0 +1,14 @@ + + Colour Palette modal (concept) + + Colour Palette + Profile: current profile name + + + + + + + + + Swatches belong to the profile; preset editor uses them via From Palette. + diff --git a/src/static/images/help/devices.svg b/src/static/images/help/devices.svg new file mode 100644 index 0000000..0598b8b --- /dev/null +++ b/src/static/images/help/devices.svg @@ -0,0 +1,18 @@ + + Devices modal + + + Devices + + Identify + + Update groups + MAC + Name + AA:BB:CC:DD:EE:01 + lounge strip + Edit + AA:BB:CC:DD:EE:02 + ceiling + ESP-NOW devices appear when they announce. + diff --git a/src/static/images/help/groups.svg b/src/static/images/help/groups.svg new file mode 100644 index 0000000..bb8072d --- /dev/null +++ b/src/static/images/help/groups.svg @@ -0,0 +1,17 @@ + + Device groups modal + + + Device groups + + Group name + + Create + lounge lights + 3 devices - Edit + dj booth + 2 devices - Edit + + Search devices to add... + Pick from list - Identify group + diff --git a/src/static/images/help/header-toolbar.svg b/src/static/images/help/header-toolbar.svg new file mode 100644 index 0000000..5187199 --- /dev/null +++ b/src/static/images/help/header-toolbar.svg @@ -0,0 +1,24 @@ + + Header: tab buttons and action bar + + + Zones + + default + + lounge + + dj + Actions (Edit mode) + + Profiles + + Zones + + Presets + + Patterns + + Run mode + Active tab highlighted. Mode button shows the mode you switch to next. + diff --git a/src/static/images/help/mobile-menu.svg b/src/static/images/help/mobile-menu.svg new file mode 100644 index 0000000..5976b83 --- /dev/null +++ b/src/static/images/help/mobile-menu.svg @@ -0,0 +1,26 @@ + + Narrow screen: Menu aggregates header actions + + + + Menu + + tab + + tab + + Dropdown (same actions as desktop header) + + Run mode + Profiles + Zones + Presets + Help + + + Content area - presets as on desktop + + preset + + preset + diff --git a/src/static/images/help/patterns.svg b/src/static/images/help/patterns.svg new file mode 100644 index 0000000..78243b6 --- /dev/null +++ b/src/static/images/help/patterns.svg @@ -0,0 +1,13 @@ + + Patterns list + + + Patterns + pulse + delay 20-200 ms + rainbow + delay 10-80 ms + sparkle + delay 5-50 ms + Choose a pattern in the preset editor - n1-n8 depend on pattern. + diff --git a/src/static/images/help/preset-editor.svg b/src/static/images/help/preset-editor.svg new file mode 100644 index 0000000..9c84583 --- /dev/null +++ b/src/static/images/help/preset-editor.svg @@ -0,0 +1,31 @@ + + Preset editor modal (simplified) + + + Preset + Name + + evening glow + Pattern + + pulse + Colours + + + P + + P = palette-linked + Brightness, delay, n1-n8 + + 0-255 + Actions + + Try + + Default + + Save+Send + + Close + Try: preview without device save. Save+Send: store and push with save. + diff --git a/src/static/images/help/profiles.svg b/src/static/images/help/profiles.svg new file mode 100644 index 0000000..0a5a1e2 --- /dev/null +++ b/src/static/images/help/profiles.svg @@ -0,0 +1,15 @@ + + Profiles modal + + + Profiles + + Apply + + Create + Garden party + Clone / Delete + House default + active + Apply switches zones and presets for this profile. + diff --git a/src/static/images/help/sequences.svg b/src/static/images/help/sequences.svg new file mode 100644 index 0000000..020354a --- /dev/null +++ b/src/static/images/help/sequences.svg @@ -0,0 +1,18 @@ + + Sequence editor + + + Sequence + Lane 1 - lounge lights + + step 1 + + step 2 + + step 3 + Lane 2 - dj booth + + step 1 + Beat / Downbeat + Add lanes - assign presets per step - attach in zone editor. + diff --git a/src/static/images/help/settings.svg b/src/static/images/help/settings.svg new file mode 100644 index 0000000..080fecb --- /dev/null +++ b/src/static/images/help/settings.svg @@ -0,0 +1,18 @@ + + Settings modal + + + Settings + + Bridge + + LED Tool + USB serial + + /dev/ttyUSB0 + Wi-Fi + + Bridge-AP + connected + LED Tool: deploy - flash - serial setup + diff --git a/src/static/images/help/tab-preset-strip.svg b/src/static/images/help/tab-preset-strip.svg new file mode 100644 index 0000000..ebdce32 --- /dev/null +++ b/src/static/images/help/tab-preset-strip.svg @@ -0,0 +1,35 @@ + + Main area: brightness and preset tiles + + + + + + + + + + + + + + lounge + Brightness (global) + + + + drag to adjust + Click tile body to select on tab devices + + + warm white + + + rainbow + + + chase + + Edit + Edit mode: drag tiles to reorder + diff --git a/src/static/images/help/zones.svg b/src/static/images/help/zones.svg new file mode 100644 index 0000000..7635d27 --- /dev/null +++ b/src/static/images/help/zones.svg @@ -0,0 +1,18 @@ + + Zones editor + + + Edit zone + Device groups on this zone + + lounge lights + Presets on this zone + + warm + + pulse + Sequences on this zone + + intro build + Drag presets to reorder - presets and sequences can share a zone. + diff --git a/src/static/patterns.js b/src/static/patterns.js index b84e39e..fffde7f 100644 --- a/src/static/patterns.js +++ b/src/static/patterns.js @@ -268,10 +268,6 @@ document.addEventListener('DOMContentLoaded', () => { throw new Error((data && data.error) || 'Create failed'); } alert(data.message || 'Pattern created.'); - resetCreateForm(); - if (patternEditorModal) { - patternEditorModal.classList.remove('active'); - } await loadPatterns(); } catch (e) { console.error('Create pattern failed:', e); diff --git a/src/static/presets.js b/src/static/presets.js index 81dd7f3..4679036 100644 --- a/src/static/presets.js +++ b/src/static/presets.js @@ -1200,12 +1200,6 @@ document.addEventListener('DOMContentLoaded', () => { const label = document.createElement('span'); label.textContent = (preset && preset.name) || presetId; - const details = document.createElement('span'); - const pattern = preset && preset.pattern ? preset.pattern : '-'; - details.textContent = pattern; - details.style.color = '#aaa'; - details.style.fontSize = '0.85em'; - const editButton = document.createElement('button'); editButton.className = 'btn btn-secondary btn-small'; editButton.textContent = 'Edit'; @@ -1235,26 +1229,6 @@ document.addEventListener('DOMContentLoaded', () => { void sendPresetViaEspNow(presetId, preset || {}, []); }); - const exportButton = document.createElement('button'); - exportButton.className = 'btn btn-secondary btn-small'; - exportButton.textContent = 'Export'; - exportButton.addEventListener('click', async () => { - try { - const response = await fetch(`/presets/${presetId}/export`, { - headers: { Accept: 'application/json' }, - }); - if (!response.ok) { - throw new Error('Export failed'); - } - const bundle = await response.json(); - const safeName = ((preset && preset.name) || presetId).replace(/[^\w.-]+/g, '_'); - window.downloadJsonFile(`preset-${safeName}.json`, bundle); - } catch (error) { - console.error('Export preset failed:', error); - alert('Failed to export preset.'); - } - }); - const deleteButton = document.createElement('button'); deleteButton.className = 'btn btn-danger btn-small'; deleteButton.textContent = 'Delete'; @@ -1282,9 +1256,7 @@ document.addEventListener('DOMContentLoaded', () => { }); row.appendChild(label); - row.appendChild(details); row.appendChild(editButton); - row.appendChild(exportButton); row.appendChild(sendButton); row.appendChild(deleteButton); presetsList.appendChild(row); @@ -1415,22 +1387,6 @@ document.addEventListener('DOMContentLoaded', () => { return; } - try { - const zoneCheck = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } }); - if (zoneCheck.ok) { - const zoneDoc = await zoneCheck.json(); - if ( - typeof window.zoneAllowsPresets === 'function' && - !window.zoneAllowsPresets(zoneDoc, zoneId) - ) { - alert('This zone is for sequences only. Add a sequence from the zone Edit menu instead.'); - return; - } - } - } catch (e) { - console.warn('Could not verify zone content kind:', e); - } - // Load all presets try { const response = await fetch('/presets', { @@ -1470,11 +1426,13 @@ document.addEventListener('DOMContentLoaded', () => { modal.id = 'add-preset-to-zone-modal'; modal.innerHTML = `