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>
@@ -2,7 +2,7 @@
|
||||
<title>Header: tab buttons and action bar</title>
|
||||
<rect width="820" height="108" fill="#1a1a1a"/>
|
||||
<rect x="0" y="106" width="820" height="2" fill="#4a4a4a"/>
|
||||
<text x="16" y="28" fill="#888" font-family="sans-serif" font-size="11">Tabs</text>
|
||||
<text x="16" y="28" fill="#888" font-family="sans-serif" font-size="11">Zones</text>
|
||||
<rect x="16" y="40" width="72" height="36" rx="4" fill="#333" stroke="#666" stroke-width="1"/>
|
||||
<text x="34" y="63" fill="#ccc" font-family="sans-serif" font-size="13">default</text>
|
||||
<rect x="96" y="40" width="88" height="36" rx="4" fill="#2a4a7a" stroke="#5a8fd4" stroke-width="2"/>
|
||||
@@ -13,7 +13,7 @@
|
||||
<rect x="380" y="40" width="72" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="396" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Profiles</text>
|
||||
<rect x="458" y="40" width="52" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="470" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Tabs</text>
|
||||
<text x="470" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Zones</text>
|
||||
<rect x="516" y="40" width="64" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="524" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Presets</text>
|
||||
<rect x="586" y="40" width="78" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -1,26 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 340" width="300" height="340" role="img" aria-labelledby="t">
|
||||
<title id="t">Narrow screen: Menu aggregates header actions</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 340" width="300" height="340" role="img" aria-labelledby="mobile-menu-title">
|
||||
<title id="mobile-menu-title">Narrow screen: Menu aggregates header actions</title>
|
||||
<rect width="300" height="340" fill="#2e2e2e"/>
|
||||
<rect x="0" y="0" width="300" height="52" fill="#1a1a1a"/>
|
||||
<rect x="12" y="12" width="56" height="28" rx="4" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="22" y="30" fill="#eee" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="12">Menu <20></text>
|
||||
<text x="22" y="30" fill="#eee" font-family="sans-serif" font-size="12">Menu</text>
|
||||
<rect x="76" y="14" width="52" height="24" rx="4" fill="#333" stroke="#666" stroke-width="1"/>
|
||||
<text x="86" y="30" fill="#ccc" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="11">tab</text>
|
||||
<text x="86" y="30" fill="#ccc" font-family="sans-serif" font-size="11">tab</text>
|
||||
<rect x="136" y="14" width="52" height="24" rx="4" fill="#2a4a7a" stroke="#5a8fd4" stroke-width="1"/>
|
||||
<text x="142" y="30" fill="#fff" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="11">tab</text>
|
||||
<text x="142" y="30" fill="#fff" font-family="sans-serif" font-size="11">tab</text>
|
||||
<rect x="12" y="60" width="276" height="168" rx="4" fill="#252525" stroke="#4a4a4a" stroke-width="1"/>
|
||||
<text x="24" y="84" fill="#888" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="10">Dropdown (same actions as desktop header)</text>
|
||||
<g font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="12" fill="#e8e8e8">
|
||||
<text x="24" y="84" fill="#888" font-family="sans-serif" font-size="10">Dropdown (same actions as desktop header)</text>
|
||||
<g font-family="sans-serif" font-size="12" fill="#e8e8e8">
|
||||
<text x="24" y="108">Run mode</text>
|
||||
<text x="24" y="132">Profiles</text>
|
||||
<text x="24" y="156">Tabs</text>
|
||||
<text x="24" y="156">Zones</text>
|
||||
<text x="24" y="180">Presets</text>
|
||||
<text x="24" y="204">Help</text>
|
||||
</g>
|
||||
<rect x="12" y="240" width="276" height="80" rx="6" fill="#222" stroke="#444" stroke-width="1"/>
|
||||
<text x="24" y="268" fill="#aaa" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="10">Content area presets as on desktop</text>
|
||||
<text x="24" y="268" fill="#aaa" font-family="sans-serif" font-size="10">Content area - presets as on desktop</text>
|
||||
<rect x="24" y="278" width="80" height="32" rx="4" fill="#333" stroke="#555"/>
|
||||
<text x="36" y="298" fill="#ddd" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="11">preset</text>
|
||||
<text x="36" y="298" fill="#ddd" font-family="sans-serif" font-size="11">preset</text>
|
||||
<rect x="112" y="278" width="80" height="32" rx="4" fill="#333" stroke="#555"/>
|
||||
<text x="124" y="298" fill="#ddd" font-family="DejaVu Sans, Liberation Sans, Arial, sans-serif" font-size="11">preset</text>
|
||||
<text x="124" y="298" fill="#ddd" font-family="sans-serif" font-size="11">preset</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -872,7 +872,7 @@ class LightingController {
|
||||
this.selectTab(this.state.zone_order[0]);
|
||||
} else {
|
||||
this.currentTab = null;
|
||||
document.getElementById('zone-content').innerHTML = '<p>No tabs available. Create a new zone to get started.</p>';
|
||||
document.getElementById('zone-content').innerHTML = '<p>No zones available. Create a new zone to get started.</p>';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1010,7 +1010,7 @@ class LightingController {
|
||||
this.state.lights = {};
|
||||
this.state.zone_order = [];
|
||||
this.renderTabs();
|
||||
document.getElementById('zone-content').innerHTML = '<p>No tabs available. Create a new zone to get started.</p>';
|
||||
document.getElementById('zone-content').innerHTML = '<p>No zones available. Create a new zone to get started.</p>';
|
||||
this.updateCurrentProfileDisplay();
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -938,7 +938,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
if (!pushRes.ok) return;
|
||||
}
|
||||
editDeviceModal.classList.remove('active');
|
||||
await loadDevicesModal();
|
||||
refreshEditDeviceDebug();
|
||||
});
|
||||
}
|
||||
if (editCloseBtn) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
15
src/static/images/help/audio.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 200" width="520" height="200">
|
||||
<title>Audio beat detection</title>
|
||||
<rect width="520" height="200" fill="#1e1e1e"/>
|
||||
<rect x="40" y="16" width="440" height="168" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="44" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Audio Beat Detection</text>
|
||||
<text x="60" y="72" fill="#aaa" font-family="sans-serif" font-size="10">Input device</text>
|
||||
<rect x="60" y="78" width="200" height="24" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="68" y="94" fill="#bbb" font-family="sans-serif" font-size="10">monitor of Built-in</text>
|
||||
<rect x="60" y="112" width="120" height="36" rx="4" fill="#2a4a2a" stroke="#5a8f5a" stroke-width="2"/>
|
||||
<text x="72" y="128" fill="#afa" font-family="sans-serif" font-size="10">BPM</text>
|
||||
<text x="72" y="142" fill="#fff" font-family="sans-serif" font-size="14" font-weight="600">128</text>
|
||||
<rect x="200" y="120" width="200" height="8" rx="4" fill="#444"/>
|
||||
<rect x="200" y="120" width="140" height="8" rx="4" fill="#6a9ee2"/>
|
||||
<text x="200" y="148" fill="#888" font-family="sans-serif" font-size="9">Volume - live level meter - tap S to sync sequence</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
14
src/static/images/help/colour-palette.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 140" width="480" height="140">
|
||||
<title>Colour Palette modal (concept)</title>
|
||||
<rect width="480" height="140" fill="#2a2a2a" stroke="#555" stroke-width="1" rx="6"/>
|
||||
<text x="20" y="28" fill="#fff" font-family="sans-serif" font-size="15" font-weight="600">Colour Palette</text>
|
||||
<text x="20" y="48" fill="#888" font-family="sans-serif" font-size="10">Profile: current profile name</text>
|
||||
<rect x="20" y="58" width="44" height="44" rx="4" fill="#e53935" stroke="#333"/>
|
||||
<rect x="72" y="58" width="44" height="44" rx="4" fill="#fdd835" stroke="#333"/>
|
||||
<rect x="124" y="58" width="44" height="44" rx="4" fill="#43a047" stroke="#333"/>
|
||||
<rect x="176" y="58" width="44" height="44" rx="4" fill="#1e88e5" stroke="#333"/>
|
||||
<rect x="228" y="58" width="44" height="44" rx="4" fill="#8e24aa" stroke="#333"/>
|
||||
<rect x="280" y="70" width="36" height="28" rx="3" fill="#1a1a1a" stroke="#666"/>
|
||||
<text x="288" y="88" fill="#ccc" font-family="sans-serif" font-size="10">+</text>
|
||||
<text x="20" y="122" fill="#aaa" font-family="sans-serif" font-size="10">Swatches belong to the profile; preset editor uses them via From Palette.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
18
src/static/images/help/devices.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 200" width="520" height="200">
|
||||
<title>Devices modal</title>
|
||||
<rect width="520" height="200" fill="#1e1e1e"/>
|
||||
<rect x="40" y="20" width="440" height="160" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="48" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Devices</text>
|
||||
<rect x="60" y="58" width="72" height="24" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="72" y="74" fill="#eee" font-family="sans-serif" font-size="10">Identify</text>
|
||||
<rect x="140" y="58" width="96" height="24" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="150" y="74" fill="#eee" font-family="sans-serif" font-size="10">Update groups</text>
|
||||
<text x="60" y="104" fill="#aaa" font-family="sans-serif" font-size="10">MAC</text>
|
||||
<text x="200" y="104" fill="#aaa" font-family="sans-serif" font-size="10">Name</text>
|
||||
<text x="60" y="128" fill="#ddd" font-family="monospace" font-size="11">AA:BB:CC:DD:EE:01</text>
|
||||
<text x="200" y="128" fill="#ddd" font-family="sans-serif" font-size="11">lounge strip</text>
|
||||
<text x="380" y="128" fill="#888" font-family="sans-serif" font-size="10">Edit</text>
|
||||
<text x="60" y="156" fill="#ddd" font-family="monospace" font-size="11">AA:BB:CC:DD:EE:02</text>
|
||||
<text x="200" y="156" fill="#ddd" font-family="sans-serif" font-size="11">ceiling</text>
|
||||
<text x="60" y="176" fill="#888" font-family="sans-serif" font-size="10">ESP-NOW devices appear when they announce.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
17
src/static/images/help/groups.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 220" width="520" height="220">
|
||||
<title>Device groups modal</title>
|
||||
<rect width="520" height="220" fill="#1e1e1e"/>
|
||||
<rect x="40" y="16" width="440" height="188" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="44" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Device groups</text>
|
||||
<rect x="60" y="54" width="140" height="24" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="68" y="70" fill="#bbb" font-family="sans-serif" font-size="11">Group name</text>
|
||||
<rect x="210" y="54" width="56" height="24" rx="3" fill="#4a4a6a" stroke="#7a7aaf" stroke-width="1"/>
|
||||
<text x="224" y="70" fill="#eee" font-family="sans-serif" font-size="11">Create</text>
|
||||
<text x="60" y="104" fill="#ccc" font-family="sans-serif" font-size="12">lounge lights</text>
|
||||
<text x="300" y="104" fill="#888" font-family="sans-serif" font-size="10">3 devices - Edit</text>
|
||||
<text x="60" y="132" fill="#ccc" font-family="sans-serif" font-size="12">dj booth</text>
|
||||
<text x="300" y="132" fill="#888" font-family="sans-serif" font-size="10">2 devices - Edit</text>
|
||||
<rect x="60" y="148" width="360" height="44" rx="4" fill="#252525" stroke="#444" stroke-width="1"/>
|
||||
<text x="72" y="168" fill="#aaa" font-family="sans-serif" font-size="10">Search devices to add...</text>
|
||||
<text x="72" y="184" fill="#888" font-family="sans-serif" font-size="9">Pick from list - Identify group</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
24
src/static/images/help/header-toolbar.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 820 108" width="820" height="108">
|
||||
<title>Header: tab buttons and action bar</title>
|
||||
<rect width="820" height="108" fill="#1a1a1a"/>
|
||||
<rect x="0" y="106" width="820" height="2" fill="#4a4a4a"/>
|
||||
<text x="16" y="28" fill="#888" font-family="sans-serif" font-size="11">Zones</text>
|
||||
<rect x="16" y="40" width="72" height="36" rx="4" fill="#333" stroke="#666" stroke-width="1"/>
|
||||
<text x="34" y="63" fill="#ccc" font-family="sans-serif" font-size="13">default</text>
|
||||
<rect x="96" y="40" width="88" height="36" rx="4" fill="#2a4a7a" stroke="#5a8fd4" stroke-width="2"/>
|
||||
<text x="108" y="63" fill="#fff" font-family="sans-serif" font-size="13">lounge</text>
|
||||
<rect x="192" y="40" width="56" height="36" rx="4" fill="#333" stroke="#666" stroke-width="1"/>
|
||||
<text x="204" y="63" fill="#ccc" font-family="sans-serif" font-size="13">dj</text>
|
||||
<text x="380" y="28" fill="#888" font-family="sans-serif" font-size="11">Actions (Edit mode)</text>
|
||||
<rect x="380" y="40" width="72" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="396" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Profiles</text>
|
||||
<rect x="458" y="40" width="52" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="470" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Zones</text>
|
||||
<rect x="516" y="40" width="64" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="524" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Presets</text>
|
||||
<rect x="586" y="40" width="78" height="30" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="598" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Patterns</text>
|
||||
<rect x="670" y="40" width="134" height="30" rx="3" fill="#4a4a6a" stroke="#7a7aaf" stroke-width="1"/>
|
||||
<text x="688" y="59" fill="#e0e0e0" font-family="sans-serif" font-size="11">Run mode</text>
|
||||
<text x="16" y="98" fill="#aaa" font-family="sans-serif" font-size="10">Active tab highlighted. Mode button shows the mode you switch to next.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
26
src/static/images/help/mobile-menu.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 340" width="300" height="340" role="img" aria-labelledby="mobile-menu-title">
|
||||
<title id="mobile-menu-title">Narrow screen: Menu aggregates header actions</title>
|
||||
<rect width="300" height="340" fill="#2e2e2e"/>
|
||||
<rect x="0" y="0" width="300" height="52" fill="#1a1a1a"/>
|
||||
<rect x="12" y="12" width="56" height="28" rx="4" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="22" y="30" fill="#eee" font-family="sans-serif" font-size="12">Menu</text>
|
||||
<rect x="76" y="14" width="52" height="24" rx="4" fill="#333" stroke="#666" stroke-width="1"/>
|
||||
<text x="86" y="30" fill="#ccc" font-family="sans-serif" font-size="11">tab</text>
|
||||
<rect x="136" y="14" width="52" height="24" rx="4" fill="#2a4a7a" stroke="#5a8fd4" stroke-width="1"/>
|
||||
<text x="142" y="30" fill="#fff" font-family="sans-serif" font-size="11">tab</text>
|
||||
<rect x="12" y="60" width="276" height="168" rx="4" fill="#252525" stroke="#4a4a4a" stroke-width="1"/>
|
||||
<text x="24" y="84" fill="#888" font-family="sans-serif" font-size="10">Dropdown (same actions as desktop header)</text>
|
||||
<g font-family="sans-serif" font-size="12" fill="#e8e8e8">
|
||||
<text x="24" y="108">Run mode</text>
|
||||
<text x="24" y="132">Profiles</text>
|
||||
<text x="24" y="156">Zones</text>
|
||||
<text x="24" y="180">Presets</text>
|
||||
<text x="24" y="204">Help</text>
|
||||
</g>
|
||||
<rect x="12" y="240" width="276" height="80" rx="6" fill="#222" stroke="#444" stroke-width="1"/>
|
||||
<text x="24" y="268" fill="#aaa" font-family="sans-serif" font-size="10">Content area - presets as on desktop</text>
|
||||
<rect x="24" y="278" width="80" height="32" rx="4" fill="#333" stroke="#555"/>
|
||||
<text x="36" y="298" fill="#ddd" font-family="sans-serif" font-size="11">preset</text>
|
||||
<rect x="112" y="278" width="80" height="32" rx="4" fill="#333" stroke="#555"/>
|
||||
<text x="124" y="298" fill="#ddd" font-family="sans-serif" font-size="11">preset</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
13
src/static/images/help/patterns.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 180" width="520" height="180">
|
||||
<title>Patterns list</title>
|
||||
<rect width="520" height="180" fill="#1e1e1e"/>
|
||||
<rect x="40" y="16" width="440" height="148" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="44" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Patterns</text>
|
||||
<text x="60" y="72" fill="#ccc" font-family="sans-serif" font-size="12">pulse</text>
|
||||
<text x="300" y="72" fill="#888" font-family="sans-serif" font-size="10">delay 20-200 ms</text>
|
||||
<text x="60" y="98" fill="#ccc" font-family="sans-serif" font-size="12">rainbow</text>
|
||||
<text x="300" y="98" fill="#888" font-family="sans-serif" font-size="10">delay 10-80 ms</text>
|
||||
<text x="60" y="124" fill="#ccc" font-family="sans-serif" font-size="12">sparkle</text>
|
||||
<text x="300" y="124" fill="#888" font-family="sans-serif" font-size="10">delay 5-50 ms</text>
|
||||
<text x="60" y="152" fill="#888" font-family="sans-serif" font-size="9">Choose a pattern in the preset editor - n1-n8 depend on pattern.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
31
src/static/images/help/preset-editor.svg
Normal file
@@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 400" width="520" height="400">
|
||||
<title>Preset editor modal (simplified)</title>
|
||||
<rect width="520" height="400" fill="#1e1e1e"/>
|
||||
<rect x="40" y="28" width="440" height="344" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="58" fill="#fff" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="17" font-weight="600">Preset</text>
|
||||
<text x="60" y="86" fill="#aaa" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Name</text>
|
||||
<rect x="60" y="92" width="200" height="28" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="72" y="111" fill="#ddd" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="12">evening glow</text>
|
||||
<text x="280" y="86" fill="#aaa" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Pattern</text>
|
||||
<rect x="280" y="92" width="160" height="28" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="292" y="111" fill="#ddd" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="12">pulse</text>
|
||||
<text x="60" y="148" fill="#aaa" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Colours</text>
|
||||
<rect x="60" y="156" width="48" height="48" rx="4" fill="#7e57c2" stroke="#333" stroke-width="1"/>
|
||||
<circle cx="66" cy="162" r="8" fill="#3f51b5" stroke="#fff" stroke-width="1"/>
|
||||
<text x="63" y="166" fill="#fff" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="7" font-weight="700">P</text>
|
||||
<rect x="116" y="156" width="48" height="48" rx="4" fill="#26a69a" stroke="#333" stroke-width="1"/>
|
||||
<text x="176" y="184" fill="#888" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="10">P = palette-linked</text>
|
||||
<text x="60" y="232" fill="#aaa" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Brightness, delay, n1-n8</text>
|
||||
<rect x="60" y="238" width="120" height="24" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="68" y="254" fill="#888" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">0-255</text>
|
||||
<text x="60" y="290" fill="#aaa" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="10">Actions</text>
|
||||
<rect x="60" y="298" width="44" height="26" rx="3" fill="#444" stroke="#666" stroke-width="1"/>
|
||||
<text x="72" y="315" fill="#eee" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Try</text>
|
||||
<rect x="112" y="298" width="56" height="26" rx="3" fill="#444" stroke="#666" stroke-width="1"/>
|
||||
<text x="120" y="315" fill="#eee" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Default</text>
|
||||
<rect x="176" y="298" width="88" height="26" rx="3" fill="#3d5a80" stroke="#5a7ab8" stroke-width="1"/>
|
||||
<text x="188" y="315" fill="#fff" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Save+Send</text>
|
||||
<rect x="272" y="298" width="48" height="26" rx="3" fill="#444" stroke="#666" stroke-width="1"/>
|
||||
<text x="284" y="315" fill="#eee" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="11">Close</text>
|
||||
<text x="60" y="352" fill="#888" font-family="DejaVu Sans,Liberation Sans,Arial,sans-serif" font-size="9">Try: preview without device save. Save+Send: store and push with save.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
15
src/static/images/help/profiles.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 200" width="520" height="200">
|
||||
<title>Profiles modal</title>
|
||||
<rect width="520" height="200" fill="#1e1e1e"/>
|
||||
<rect x="40" y="20" width="440" height="160" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="48" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Profiles</text>
|
||||
<rect x="60" y="62" width="48" height="26" rx="3" fill="#4a4a6a" stroke="#7a7aaf" stroke-width="1"/>
|
||||
<text x="72" y="79" fill="#eee" font-family="sans-serif" font-size="11">Apply</text>
|
||||
<rect x="116" y="62" width="52" height="26" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="126" y="79" fill="#eee" font-family="sans-serif" font-size="11">Create</text>
|
||||
<text x="60" y="108" fill="#ccc" font-family="sans-serif" font-size="12">Garden party</text>
|
||||
<text x="360" y="108" fill="#888" font-family="sans-serif" font-size="10">Clone / Delete</text>
|
||||
<text x="60" y="136" fill="#ccc" font-family="sans-serif" font-size="12">House default</text>
|
||||
<text x="360" y="136" fill="#6a9ee2" font-family="sans-serif" font-size="10">active</text>
|
||||
<text x="60" y="168" fill="#888" font-family="sans-serif" font-size="10">Apply switches zones and presets for this profile.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
18
src/static/images/help/sequences.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 200" width="520" height="200">
|
||||
<title>Sequence editor</title>
|
||||
<rect width="520" height="200" fill="#1e1e1e"/>
|
||||
<rect x="40" y="16" width="440" height="168" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="44" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Sequence</text>
|
||||
<text x="60" y="68" fill="#aaa" font-family="sans-serif" font-size="10">Lane 1 - lounge lights</text>
|
||||
<rect x="60" y="74" width="56" height="28" rx="3" fill="#4a3a6a" stroke="#7a6aaf" stroke-width="1"/>
|
||||
<text x="72" y="92" fill="#eee" font-family="sans-serif" font-size="9">step 1</text>
|
||||
<rect x="122" y="74" width="56" height="28" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="134" y="92" fill="#eee" font-family="sans-serif" font-size="9">step 2</text>
|
||||
<rect x="184" y="74" width="56" height="28" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="196" y="92" fill="#eee" font-family="sans-serif" font-size="9">step 3</text>
|
||||
<text x="60" y="124" fill="#aaa" font-family="sans-serif" font-size="10">Lane 2 - dj booth</text>
|
||||
<rect x="60" y="130" width="56" height="28" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="72" y="148" fill="#eee" font-family="sans-serif" font-size="9">step 1</text>
|
||||
<text x="300" y="44" fill="#888" font-family="sans-serif" font-size="9">Beat / Downbeat</text>
|
||||
<text x="60" y="176" fill="#888" font-family="sans-serif" font-size="9">Add lanes - assign presets per step - attach in zone editor.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
18
src/static/images/help/settings.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 200" width="520" height="200">
|
||||
<title>Settings modal</title>
|
||||
<rect width="520" height="200" fill="#1e1e1e"/>
|
||||
<rect x="40" y="16" width="440" height="168" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="44" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Settings</text>
|
||||
<rect x="60" y="52" width="56" height="24" rx="3" fill="#1a1a1a" stroke="#6a5acd" stroke-width="2"/>
|
||||
<text x="72" y="68" fill="#fff" font-family="sans-serif" font-size="10">Bridge</text>
|
||||
<rect x="122" y="52" width="64" height="24" rx="3" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="132" y="68" fill="#ccc" font-family="sans-serif" font-size="10">LED Tool</text>
|
||||
<text x="60" y="100" fill="#aaa" font-family="sans-serif" font-size="10">USB serial</text>
|
||||
<rect x="60" y="106" width="160" height="22" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="68" y="121" fill="#bbb" font-family="sans-serif" font-size="9">/dev/ttyUSB0</text>
|
||||
<text x="60" y="144" fill="#aaa" font-family="sans-serif" font-size="10">Wi-Fi</text>
|
||||
<rect x="60" y="150" width="120" height="22" rx="3" fill="#1a1a1a" stroke="#444" stroke-width="1"/>
|
||||
<text x="68" y="165" fill="#bbb" font-family="sans-serif" font-size="9">Bridge-AP</text>
|
||||
<text x="240" y="121" fill="#6a9e6a" font-family="sans-serif" font-size="9">connected</text>
|
||||
<text x="240" y="165" fill="#888" font-family="sans-serif" font-size="9">LED Tool: deploy - flash - serial setup</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
35
src/static/images/help/tab-preset-strip.svg
Normal file
@@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 220" width="800" height="220">
|
||||
<title>Main area: brightness and preset tiles</title>
|
||||
<defs>
|
||||
<linearGradient id="rg1" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#ffd54f"/><stop offset="100%" style="stop-color:#fff8e1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="rg2" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#e53935"/><stop offset="33%" style="stop-color:#fdd835"/>
|
||||
<stop offset="66%" style="stop-color:#43a047"/><stop offset="100%" style="stop-color:#1e88e5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="rg3" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" style="stop-color:#00897b"/><stop offset="100%" style="stop-color:#4db6ac"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="800" height="220" fill="#2e2e2e"/>
|
||||
<text x="20" y="32" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">lounge</text>
|
||||
<text x="20" y="56" fill="#aaa" font-family="sans-serif" font-size="11">Brightness (global)</text>
|
||||
<rect x="20" y="64" width="320" height="8" rx="4" fill="#444"/>
|
||||
<rect x="20" y="64" width="200" height="8" rx="4" fill="#6a9ee2"/>
|
||||
<circle cx="220" cy="68" r="10" fill="#ccc" stroke="#333" stroke-width="1"/>
|
||||
<text x="360" y="74" fill="#888" font-family="sans-serif" font-size="11">drag to adjust</text>
|
||||
<text x="20" y="108" fill="#aaa" font-family="sans-serif" font-size="11">Click tile body to select on tab devices</text>
|
||||
<rect x="20" y="118" width="200" height="72" rx="6" fill="#252525" stroke="#555" stroke-width="1"/>
|
||||
<rect x="28" y="128" width="184" height="36" rx="4" fill="url(#rg1)"/>
|
||||
<text x="32" y="180" fill="#eee" font-family="sans-serif" font-size="13" font-weight="600">warm white</text>
|
||||
<rect x="232" y="118" width="200" height="72" rx="6" fill="#252525" stroke="#6a9ee2" stroke-width="2"/>
|
||||
<rect x="240" y="128" width="184" height="36" rx="4" fill="url(#rg2)"/>
|
||||
<text x="244" y="180" fill="#eee" font-family="sans-serif" font-size="13" font-weight="600">rainbow</text>
|
||||
<rect x="444" y="118" width="200" height="72" rx="6" fill="#252525" stroke="#555" stroke-width="1"/>
|
||||
<rect x="452" y="128" width="184" height="36" rx="4" fill="url(#rg3)"/>
|
||||
<text x="456" y="180" fill="#eee" font-family="sans-serif" font-size="13" font-weight="600">chase</text>
|
||||
<rect x="656" y="130" width="56" height="48" rx="4" fill="#3d3d3d" stroke="#555" stroke-width="1"/>
|
||||
<text x="670" y="158" fill="#ddd" font-family="sans-serif" font-size="11">Edit</text>
|
||||
<text x="656" y="198" fill="#888" font-family="sans-serif" font-size="10">Edit mode: drag tiles to reorder</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
18
src/static/images/help/zones.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 520 240" width="520" height="240">
|
||||
<title>Zones editor</title>
|
||||
<rect width="520" height="240" fill="#1e1e1e"/>
|
||||
<rect x="40" y="16" width="440" height="208" rx="8" fill="#2a2a2a" stroke="#555" stroke-width="1"/>
|
||||
<text x="60" y="44" fill="#fff" font-family="sans-serif" font-size="16" font-weight="600">Edit zone</text>
|
||||
<text x="60" y="72" fill="#aaa" font-family="sans-serif" font-size="10">Device groups on this zone</text>
|
||||
<rect x="60" y="78" width="100" height="22" rx="3" fill="#333" stroke="#666" stroke-width="1"/>
|
||||
<text x="72" y="93" fill="#ccc" font-family="sans-serif" font-size="10">lounge lights</text>
|
||||
<text x="60" y="118" fill="#aaa" font-family="sans-serif" font-size="10">Presets on this zone</text>
|
||||
<rect x="60" y="124" width="72" height="36" rx="4" fill="#3d3d3d" stroke="#666" stroke-width="1"/>
|
||||
<text x="72" y="147" fill="#eee" font-family="sans-serif" font-size="10">warm</text>
|
||||
<rect x="140" y="124" width="72" height="36" rx="4" fill="#3d3d3d" stroke="#666" stroke-width="1"/>
|
||||
<text x="148" y="147" fill="#eee" font-family="sans-serif" font-size="10">pulse</text>
|
||||
<text x="60" y="178" fill="#aaa" font-family="sans-serif" font-size="10">Sequences on this zone</text>
|
||||
<rect x="60" y="184" width="120" height="28" rx="4" fill="#2a4a3a" stroke="#5a8f6a" stroke-width="1"/>
|
||||
<text x="72" y="202" fill="#cfe" font-family="sans-serif" font-size="10">intro build</text>
|
||||
<text x="60" y="228" fill="#888" font-family="sans-serif" font-size="9">Drag presets to reorder - presets and sequences can share a zone.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -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);
|
||||
|
||||
@@ -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 = `
|
||||
<div class="modal-content">
|
||||
<h2>Add Preset to Zone</h2>
|
||||
<div id="add-preset-list" class="profiles-list" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="add-preset-to-zone-close-btn">Close</button>
|
||||
<div class="modal-head">
|
||||
<h2>Add Preset to Zone</h2>
|
||||
<div class="modal-top-actions">
|
||||
<button class="btn btn-secondary" id="add-preset-to-zone-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="add-preset-list" class="profiles-list" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1559,13 +1517,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(tabData, zoneId)
|
||||
) {
|
||||
alert('This zone is for sequences only. Add a sequence from the zone Edit menu instead.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize to flat array to check and update usage
|
||||
let flat = [];
|
||||
@@ -1686,11 +1637,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
modal.className = 'modal active modal-child-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<h2>Pick Palette Color</h2>
|
||||
<div id="pick-palette-list" class="profiles-list" style="max-height: 300px; overflow-y: auto;"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="pick-palette-close-btn">Close</button>
|
||||
<div class="modal-head">
|
||||
<h2>Pick Palette Color</h2>
|
||||
<div class="modal-top-actions">
|
||||
<button class="btn btn-secondary" id="pick-palette-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pick-palette-list" class="profiles-list" style="max-height: 300px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
@@ -1755,11 +1708,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
modal.className = 'modal active modal-child-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<h2>Pick background colour</h2>
|
||||
<div id="pick-bg-palette-list" class="profiles-list" style="max-height: 300px; overflow-y: auto;"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="pick-bg-palette-close-btn">Close</button>
|
||||
<div class="modal-head">
|
||||
<h2>Pick background colour</h2>
|
||||
<div class="modal-top-actions">
|
||||
<button class="btn btn-secondary" id="pick-bg-palette-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pick-bg-palette-list" class="profiles-list" style="max-height: 300px; overflow-y: auto;"></div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
@@ -1866,32 +1821,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
throw new Error('Failed to save preset');
|
||||
}
|
||||
|
||||
// Same device targeting as Try: per-preset zone groups when in a zone tab.
|
||||
const presetIdForSend = currentEditId || payload.name;
|
||||
const deviceNames = await deviceNamesForPresetOnCurrentZone(presetIdForSend);
|
||||
|
||||
// Use saved preset from server response for sending
|
||||
const saved = await response.json().catch(() => null);
|
||||
if (saved && typeof saved === 'object') {
|
||||
if (currentEditId) {
|
||||
// PUT returns the preset object directly; use the existing ID
|
||||
await sendPresetViaEspNow(currentEditId, saved, deviceNames, false, false, '2');
|
||||
} else {
|
||||
// POST returns { id: preset }
|
||||
const entries = Object.entries(saved);
|
||||
if (entries.length > 0) {
|
||||
const [newId, presetData] = entries[0];
|
||||
await sendPresetViaEspNow(newId, presetData, deviceNames, false, false, '2');
|
||||
}
|
||||
if (!currentEditId && saved && typeof saved === 'object') {
|
||||
const entries = Object.entries(saved);
|
||||
if (entries.length > 0) {
|
||||
currentEditId = entries[0][0];
|
||||
}
|
||||
} else {
|
||||
// Fallback: send what we just built
|
||||
await sendPresetViaEspNow(currentEditId || payload.name, payload, deviceNames, false, false, '2');
|
||||
}
|
||||
|
||||
await loadPresets();
|
||||
clearForm();
|
||||
closeEditor();
|
||||
|
||||
// Reload zone presets if we're in a zone view
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
@@ -2195,13 +2133,29 @@ async function sendZonePresetSelection(zoneId, tabData, presetId, preset, allPre
|
||||
const selectedPresets = {};
|
||||
// Store selected preset payload per zone for beat-trigger reliability.
|
||||
const selectedPresetPayloads = {};
|
||||
// Run vs Edit for zone preset strip (in-memory only — each full page load starts in run mode)
|
||||
let presetUiMode = 'run';
|
||||
const PRESET_UI_MODE_STORAGE_KEY = 'led-controller-ui-mode';
|
||||
|
||||
function readStoredPresetUiMode() {
|
||||
try {
|
||||
const stored = localStorage.getItem(PRESET_UI_MODE_STORAGE_KEY);
|
||||
return stored === 'edit' ? 'edit' : 'run';
|
||||
} catch (_) {
|
||||
return 'run';
|
||||
}
|
||||
}
|
||||
|
||||
// Run vs Edit for zone preset strip (restored from localStorage on load)
|
||||
let presetUiMode = readStoredPresetUiMode();
|
||||
|
||||
const getPresetUiMode = () => (presetUiMode === 'edit' ? 'edit' : 'run');
|
||||
|
||||
const setPresetUiMode = (mode) => {
|
||||
presetUiMode = mode === 'edit' ? 'edit' : 'run';
|
||||
try {
|
||||
localStorage.setItem(PRESET_UI_MODE_STORAGE_KEY, presetUiMode);
|
||||
} catch (_) {
|
||||
/* ignore quota / private mode */
|
||||
}
|
||||
};
|
||||
|
||||
const updateUiModeToggleButtons = () => {
|
||||
@@ -2216,6 +2170,11 @@ const updateUiModeToggleButtons = () => {
|
||||
document.body.classList.toggle('preset-ui-edit', mode === 'edit');
|
||||
document.body.classList.toggle('preset-ui-run', mode === 'run');
|
||||
};
|
||||
|
||||
if (typeof document !== 'undefined' && document.body) {
|
||||
updateUiModeToggleButtons();
|
||||
}
|
||||
|
||||
// Track if we're currently dragging a preset
|
||||
let isDraggingPreset = false;
|
||||
|
||||
@@ -2273,12 +2232,6 @@ const savePresetGrid = async (zoneId, presetGrid) => {
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(tabData, zoneId)
|
||||
) {
|
||||
throw new Error('This zone is for sequences only.');
|
||||
}
|
||||
|
||||
// Store as 2D grid
|
||||
tabData.presets = presetGrid;
|
||||
@@ -2372,12 +2325,6 @@ const renderTabPresets = async (zoneId, options = {}) => {
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
const groupsMapStrip = groupsStripRes.ok ? await groupsStripRes.json() : {};
|
||||
const ck =
|
||||
typeof window.effectiveZoneContentKind === 'function'
|
||||
? window.effectiveZoneContentKind(tabData)
|
||||
: typeof window.normalizeZoneContentKind === 'function'
|
||||
? window.normalizeZoneContentKind(tabData)
|
||||
: 'presets';
|
||||
|
||||
// Get presets - support both 2D grid and flat array (for backward compatibility)
|
||||
let presetGrid = tabData.presets;
|
||||
@@ -2389,10 +2336,6 @@ const renderTabPresets = async (zoneId, options = {}) => {
|
||||
// It's a flat array, convert to grid
|
||||
presetGrid = arrayToGrid(presetGrid, 3);
|
||||
}
|
||||
if (ck === 'sequences') {
|
||||
presetGrid = [];
|
||||
}
|
||||
|
||||
if (!presetsResponse.ok) {
|
||||
throw new Error('Failed to load presets');
|
||||
}
|
||||
@@ -2474,18 +2417,12 @@ const renderTabPresets = async (zoneId, options = {}) => {
|
||||
tabData.sequence_ids.some((x) => x != null && String(x).trim());
|
||||
|
||||
if (flatPresets.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.style.gridColumn = '1 / -1'; // Span all columns
|
||||
if (ck === 'sequences') {
|
||||
if (!hasSeq) {
|
||||
empty.textContent =
|
||||
"No sequences on this zone yet. Open the zone's Edit menu to add one.";
|
||||
presetsList.appendChild(empty);
|
||||
}
|
||||
} else {
|
||||
if (!hasSeq) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.style.gridColumn = '1 / -1';
|
||||
empty.textContent =
|
||||
'No presets added to this zone. Open the zone\'s Edit menu and click "Add Preset" to add one.';
|
||||
"No presets or sequences on this zone yet. Open Edit to add presets or sequences.";
|
||||
presetsList.appendChild(empty);
|
||||
}
|
||||
} else {
|
||||
@@ -2515,11 +2452,7 @@ const renderTabPresets = async (zoneId, options = {}) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
typeof window.appendZoneSequenceTiles === 'function' &&
|
||||
(typeof window.zoneAllowsSequences !== 'function' ||
|
||||
window.zoneAllowsSequences(tabData, zoneId))
|
||||
) {
|
||||
if (typeof window.appendZoneSequenceTiles === 'function') {
|
||||
await window.appendZoneSequenceTiles(zoneId, tabData, allPresets, paletteColors, presetsList);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2760,13 +2693,6 @@ const removePresetFromTab = async (zoneId, presetId) => {
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
if (
|
||||
typeof window.zoneAllowsPresets === 'function' &&
|
||||
!window.zoneAllowsPresets(tabData, zoneId)
|
||||
) {
|
||||
alert('This zone is for sequences only.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize to flat array
|
||||
let flat = [];
|
||||
|
||||
@@ -13,6 +13,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
}
|
||||
|
||||
const isEditModeActive = () => {
|
||||
if (typeof window.getPresetUiMode === 'function') {
|
||||
return window.getPresetUiMode() === 'edit';
|
||||
}
|
||||
const toggle = document.querySelector('.ui-mode-toggle');
|
||||
return !!(toggle && toggle.getAttribute('aria-pressed') === 'true');
|
||||
};
|
||||
|
||||
@@ -510,13 +510,6 @@ async function addSequenceToTab(sequenceId, zoneId) {
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
|
||||
if (!tabResponse.ok) throw new Error('Failed to load zone');
|
||||
const tabData = await tabResponse.json();
|
||||
if (
|
||||
typeof window.zoneAllowsSequences === 'function' &&
|
||||
!window.zoneAllowsSequences(tabData, zoneId)
|
||||
) {
|
||||
alert('This zone is for presets only. Add presets from the zone Edit menu instead.');
|
||||
return;
|
||||
}
|
||||
const list = Array.isArray(tabData.sequence_ids) ? tabData.sequence_ids.map(String) : [];
|
||||
if (list.includes(String(sequenceId))) {
|
||||
alert('Sequence is already on this zone.');
|
||||
@@ -579,15 +572,6 @@ async function refreshEditTabSequencesUi(zoneId) {
|
||||
const zoneRes = await fetch(`/zones/${zoneId}`, { headers: { Accept: 'application/json' } });
|
||||
if (!zoneRes.ok) throw new Error('zone');
|
||||
const zone = await zoneRes.json();
|
||||
if (
|
||||
typeof window.zoneAllowsSequences === 'function' &&
|
||||
!window.zoneAllowsSequences(zone, zoneId)
|
||||
) {
|
||||
currentEl.innerHTML =
|
||||
'<span class="muted-text">This zone is for presets only. Sequences are hidden.</span>';
|
||||
addEl.innerHTML = '<span class="muted-text">—</span>';
|
||||
return;
|
||||
}
|
||||
const onZone = Array.isArray(zone.sequence_ids) ? zone.sequence_ids.map(String) : [];
|
||||
const seqMap = await fetchSequencesMap();
|
||||
const onSet = new Set(onZone);
|
||||
@@ -600,11 +584,7 @@ async function refreshEditTabSequencesUi(zoneId) {
|
||||
const sdoc = seqMap[sid] || {};
|
||||
const name = sdoc.name || sid;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profiles-row';
|
||||
row.style.display = 'flex';
|
||||
row.style.justifyContent = 'space-between';
|
||||
row.style.alignItems = 'center';
|
||||
row.style.gap = '0.5rem';
|
||||
row.className = 'profiles-row edit-zone-item-row';
|
||||
const span = document.createElement('span');
|
||||
span.textContent = `${name} — ${sid}`;
|
||||
const rm = document.createElement('button');
|
||||
@@ -1081,9 +1061,16 @@ async function saveSequenceEditor() {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error((err && err.error) || res.statusText);
|
||||
}
|
||||
const created = await res.json().catch(() => null);
|
||||
if (created && typeof created === 'object') {
|
||||
const entries = Object.entries(created);
|
||||
if (entries.length > 0) {
|
||||
sequenceEditorId = String(entries[0][0]);
|
||||
const edDel = document.getElementById('sequence-editor-delete-btn');
|
||||
if (edDel) edDel.style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
}
|
||||
document.getElementById('sequence-editor-modal') && document.getElementById('sequence-editor-modal').classList.remove('active');
|
||||
stopSequenceEditorBpmPoll();
|
||||
await loadSequencesModalList();
|
||||
const zid = resolveZoneIdForPresetStripRefresh();
|
||||
if (zid && typeof window.refreshEditTabSequencesUi === 'function') {
|
||||
@@ -1164,31 +1151,12 @@ async function loadSequencesModalList() {
|
||||
const nSteps = ln.reduce((a, l) => a + l.length, 0);
|
||||
const nLanes = ln.filter((l) => l.length > 0).length || 1;
|
||||
title.textContent = `${doc.name || id} — ${nLanes} lane(s), ${nSteps} step(s)`;
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.type = 'button';
|
||||
exportBtn.className = 'btn btn-secondary btn-small';
|
||||
exportBtn.textContent = 'Export';
|
||||
exportBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
const response = await fetch(`/sequences/${id}/export`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) throw new Error('Export failed');
|
||||
const bundle = await response.json();
|
||||
const safeName = String(doc.name || id).replace(/[^\w.-]+/g, '_');
|
||||
window.downloadJsonFile(`sequence-${safeName}.json`, bundle);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('Failed to export sequence.');
|
||||
}
|
||||
});
|
||||
const edit = document.createElement('button');
|
||||
edit.type = 'button';
|
||||
edit.className = 'btn btn-secondary btn-small';
|
||||
edit.textContent = 'Edit';
|
||||
edit.addEventListener('click', () => openSequenceEditor(id, doc));
|
||||
row.appendChild(title);
|
||||
row.appendChild(exportBtn);
|
||||
row.appendChild(edit);
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
@@ -1227,33 +1195,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
openSequenceEditor(null, null);
|
||||
});
|
||||
}
|
||||
const importSeqBtn = document.getElementById('import-sequence-btn');
|
||||
if (importSeqBtn) {
|
||||
importSeqBtn.addEventListener('click', async () => {
|
||||
const text = await window.pickJsonFile();
|
||||
if (!text) return;
|
||||
const bundle = window.parseJsonFileText(text);
|
||||
if (!bundle || bundle.kind !== 'sequence') {
|
||||
alert('Invalid sequence bundle file.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch('/sequences/import', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ bundle }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.error || 'Import failed');
|
||||
}
|
||||
await loadSequencesModalList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(e.message || 'Failed to import sequence.');
|
||||
}
|
||||
});
|
||||
}
|
||||
const openPresetsFromSeq = document.getElementById('sequences-open-presets-btn');
|
||||
if (openPresetsFromSeq) {
|
||||
openPresetsFromSeq.addEventListener('click', () => {
|
||||
|
||||
@@ -125,6 +125,68 @@ header h1 {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.zones-menu-mobile {
|
||||
display: none;
|
||||
position: relative;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.zones-menu-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
padding: 0.25rem 0;
|
||||
display: none;
|
||||
min-width: 10rem;
|
||||
max-width: min(16rem, calc(100vw - 1rem));
|
||||
max-height: min(50vh, 20rem);
|
||||
overflow-y: auto;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.zones-menu-dropdown.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.zones-menu-item {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: left;
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.zones-menu-item:hover {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
.zones-menu-item.active {
|
||||
background-color: #6a5acd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.zones-menu-empty {
|
||||
padding: 0.45rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
#zones-menu-btn {
|
||||
max-width: 9rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-menu-mobile {
|
||||
display: none;
|
||||
position: relative;
|
||||
@@ -1444,6 +1506,29 @@ body.preset-ui-run .edit-mode-only {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.modal-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.modal-head h2 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.modal-top-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.modal-content label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
@@ -1504,7 +1589,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* On mobile, hide header buttons; all actions (including Tabs) are in the Menu dropdown */
|
||||
/* On mobile, hide header buttons; all actions (including Zones) are in the Menu dropdown */
|
||||
.header-actions {
|
||||
display: none;
|
||||
}
|
||||
@@ -1540,17 +1625,28 @@ body.preset-ui-run .edit-mode-only {
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.zones-menu-mobile {
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.zones-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-menu-mobile {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
margin-top: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.header-end {
|
||||
gap: 0.35rem;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.header-end .audio-top-indicator {
|
||||
@@ -1569,12 +1665,6 @@ body.preset-ui-run .edit-mode-only {
|
||||
padding: 0.4rem 0.7rem;
|
||||
}
|
||||
|
||||
.zones-container {
|
||||
padding: 0.35rem 0 0;
|
||||
border-bottom: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.zone-content {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
@@ -1689,6 +1779,39 @@ body.preset-ui-run .edit-mode-only {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.group-list-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
background-color: #3a3a3a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.group-list-row-info {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.group-list-row-title {
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.group-list-row-meta {
|
||||
margin-top: 0.15rem;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.35;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.group-list-row-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.zone-modal-create-row {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
@@ -1774,6 +1897,65 @@ body.preset-ui-run .edit-mode-only {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.zone-device-add-picker {
|
||||
flex: 1 1 100%;
|
||||
min-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.zone-device-add-search {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background-color: #3a3a3a;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.zone-device-add-search:focus {
|
||||
outline: none;
|
||||
border-color: #6a5acd;
|
||||
}
|
||||
|
||||
.zone-device-add-results {
|
||||
max-height: 10rem;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
background-color: #3a3a3a;
|
||||
}
|
||||
|
||||
.zone-device-add-results-empty {
|
||||
padding: 0.5rem 0.6rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.zone-device-add-result {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 0.45rem 0.6rem;
|
||||
border: none;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
background: transparent;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.zone-device-add-result:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.zone-device-add-result:hover,
|
||||
.zone-device-add-result:focus-visible {
|
||||
background-color: #4a4a4a;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.zone-devices-add {
|
||||
margin-top: 0;
|
||||
flex-wrap: wrap;
|
||||
@@ -1791,6 +1973,11 @@ body.preset-ui-run .edit-mode-only {
|
||||
overflow-y: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.edit-zone-presets-scroll .edit-zone-item-row {
|
||||
padding: 0.25rem 0.4rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
/* Hide any text content in palette rows - only show color swatches */
|
||||
#palette-container .profiles-row {
|
||||
font-size: 0; /* Hide any text nodes */
|
||||
@@ -1871,16 +2058,205 @@ body.preset-ui-run .edit-mode-only {
|
||||
}
|
||||
}
|
||||
/* Help modal readability */
|
||||
#help-modal .modal-content {
|
||||
max-width: 720px;
|
||||
#help-modal .modal-content,
|
||||
#help-modal .help-modal-content {
|
||||
max-width: 840px;
|
||||
width: 95vw;
|
||||
line-height: 1.6;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
#help-modal .modal-content h2 {
|
||||
#help-modal .modal-head {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
#help-modal .help-modal-intro {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
#help-modal .help-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0.5rem 0 1rem;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
#help-modal .help-tab-btn {
|
||||
background: #3a3a3a;
|
||||
color: #ccc;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px 4px 0 0;
|
||||
padding: 0.4rem 0.7rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
#help-modal .help-tab-btn:hover {
|
||||
color: #fff;
|
||||
border-color: #6a5acd;
|
||||
}
|
||||
#help-modal .help-tab-btn.active {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border-color: #6a5acd;
|
||||
border-bottom-color: #1a1a1a;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
#help-modal .help-tab-panel:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
#help-modal .help-tab-panel {
|
||||
max-height: min(70vh, 640px);
|
||||
overflow-y: auto;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
#help-modal .help-ui-preview {
|
||||
margin: 0 0 1rem;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 8px;
|
||||
background: #1a1a1a;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
#help-modal .help-ui-preview-caption {
|
||||
margin: -0.5rem 0 1rem;
|
||||
color: #999;
|
||||
font-size: 0.85rem;
|
||||
text-align: left;
|
||||
}
|
||||
#help-modal .help-ui-preview .help-preview-surface {
|
||||
background-color: #2e2e2e;
|
||||
padding: 1.25rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
#help-modal .help-ui-preview .modal-content {
|
||||
position: static;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 1.25rem;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
#help-modal .help-ui-preview .modal-head {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
#help-modal .help-ui-preview .profiles-list {
|
||||
max-height: none;
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
#help-modal .help-ui-preview .modal-actions {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
#help-modal .help-ui-preview--header .help-preview-header {
|
||||
background-color: #1a1a1a;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 2px solid #4a4a4a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
#help-modal .help-ui-preview--strip .zone-content {
|
||||
padding: 0.5rem 0.75rem;
|
||||
max-height: 11rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
#help-modal .help-ui-preview .help-preview-presets-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-auto-rows: minmax(5rem, auto);
|
||||
column-gap: 0.3rem;
|
||||
row-gap: 0.3rem;
|
||||
width: 100%;
|
||||
}
|
||||
#help-modal .help-ui-preview--mobile {
|
||||
max-width: 220px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
#help-modal .help-ui-preview--mobile .help-preview-mobile-bar {
|
||||
background-color: #1a1a1a;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
}
|
||||
#help-modal .help-ui-preview--mobile .main-menu-dropdown {
|
||||
display: block;
|
||||
position: static;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
#help-modal .help-ui-preview .preset-colors-container {
|
||||
min-height: 5rem;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
padding: 0.5rem;
|
||||
background-color: #2a2a2a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#help-modal .help-ui-preview .help-preview-color-swatch {
|
||||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #4a4a4a;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#help-modal .help-ui-preview .help-preview-p-badge {
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: -6px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background: #3f51b5;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
#help-modal .help-ui-preview .settings-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0 0 0.75rem;
|
||||
border-bottom: 1px solid #4a4a4a;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
#help-modal .help-ui-preview .settings-tab-btn {
|
||||
background: #3a3a3a;
|
||||
color: #ccc;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px 4px 0 0;
|
||||
padding: 0.45rem 0.85rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
#help-modal .help-ui-preview .settings-tab-btn.active {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
border-color: #6a5acd;
|
||||
}
|
||||
#help-modal .help-ui-preview .settings-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
#help-modal .help-ui-preview select {
|
||||
padding: 0.35rem 0.5rem;
|
||||
background-color: #2e2e2e;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
#help-modal .help-ui-preview .profiles-actions input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
#help-modal .modal-content h3 {
|
||||
margin-top: 1.25rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.4rem;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -22,6 +22,101 @@ function prepareZoneDevicesPanel(containerEl) {
|
||||
return { listEl, addSlot };
|
||||
}
|
||||
|
||||
/**
|
||||
* Search field + scrollable filtered list for picking an item to add.
|
||||
*/
|
||||
function createSearchableAddPicker({
|
||||
entries,
|
||||
excludeIds,
|
||||
labelFor,
|
||||
searchTextFor,
|
||||
onPick,
|
||||
placeholder = 'Search…',
|
||||
emptyMessage = 'No matches.',
|
||||
noItemsMessage = 'Nothing to add.',
|
||||
}) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'zone-device-add-picker';
|
||||
|
||||
const excluded = excludeIds || new Set();
|
||||
const available = (entries || []).filter(([id]) => !excluded.has(id));
|
||||
|
||||
if (!available.length) {
|
||||
const empty = document.createElement('span');
|
||||
empty.className = 'muted-text';
|
||||
empty.textContent = noItemsMessage;
|
||||
wrap.appendChild(empty);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const search = document.createElement('input');
|
||||
search.type = 'search';
|
||||
search.className = 'zone-device-add-search';
|
||||
search.placeholder = placeholder;
|
||||
search.setAttribute('aria-label', placeholder);
|
||||
|
||||
const results = document.createElement('div');
|
||||
results.className = 'zone-device-add-results';
|
||||
results.setAttribute('role', 'listbox');
|
||||
|
||||
const filterAvailable = (query) => {
|
||||
const q = String(query || '').trim().toLowerCase();
|
||||
return available.filter(([id, item]) => {
|
||||
if (!q) return true;
|
||||
const text = searchTextFor(id, item);
|
||||
return String(text).toLowerCase().includes(q);
|
||||
});
|
||||
};
|
||||
|
||||
const pickEntry = (id, item) => {
|
||||
onPick(id, item);
|
||||
search.value = '';
|
||||
renderResults('');
|
||||
};
|
||||
|
||||
const renderResults = (query) => {
|
||||
results.innerHTML = '';
|
||||
const filtered = filterAvailable(query);
|
||||
|
||||
if (!filtered.length) {
|
||||
const none = document.createElement('div');
|
||||
none.className = 'zone-device-add-results-empty muted-text';
|
||||
none.textContent = emptyMessage;
|
||||
results.appendChild(none);
|
||||
return;
|
||||
}
|
||||
|
||||
filtered.forEach(([id, item]) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'zone-device-add-result';
|
||||
btn.setAttribute('role', 'option');
|
||||
btn.textContent = labelFor(id, item);
|
||||
btn.addEventListener('click', () => pickEntry(id, item));
|
||||
results.appendChild(btn);
|
||||
});
|
||||
};
|
||||
|
||||
search.addEventListener('input', () => renderResults(search.value));
|
||||
search.addEventListener('focus', () => renderResults(search.value));
|
||||
|
||||
search.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Enter') return;
|
||||
const filtered = filterAvailable(search.value);
|
||||
if (filtered.length === 1) {
|
||||
event.preventDefault();
|
||||
pickEntry(filtered[0][0], filtered[0][1]);
|
||||
}
|
||||
});
|
||||
|
||||
renderResults('');
|
||||
|
||||
wrap.appendChild(search);
|
||||
wrap.appendChild(results);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.prepareZoneDevicesPanel = prepareZoneDevicesPanel;
|
||||
window.createSearchableAddPicker = createSearchableAddPicker;
|
||||
}
|
||||
|
||||
@@ -127,6 +127,9 @@ function sendZoneBrightness(zoneId, value) {
|
||||
}
|
||||
|
||||
const isEditModeActive = () => {
|
||||
if (typeof window.getPresetUiMode === 'function') {
|
||||
return window.getPresetUiMode() === 'edit';
|
||||
}
|
||||
const toggle = document.querySelector('.ui-mode-toggle');
|
||||
return !!(toggle && toggle.getAttribute('aria-pressed') === 'true');
|
||||
};
|
||||
@@ -534,27 +537,30 @@ function effectiveZoneContentKind(zoneDoc) {
|
||||
|
||||
/** @returns {boolean} */
|
||||
function zoneAllowsPresets(zoneDoc, zoneId) {
|
||||
void zoneDoc;
|
||||
void zoneId;
|
||||
return effectiveZoneContentKind(zoneDoc) === 'presets';
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @returns {boolean} */
|
||||
function zoneAllowsSequences(zoneDoc, zoneId) {
|
||||
void zoneDoc;
|
||||
void zoneId;
|
||||
return effectiveZoneContentKind(zoneDoc) === 'sequences';
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyZoneContentKindEditModal(kind) {
|
||||
function applyZoneContentKindEditModal(_kind) {
|
||||
const presetsBlock = document.getElementById('edit-zone-block-presets');
|
||||
const groupsBlock = document.getElementById('edit-zone-block-groups');
|
||||
const seqBlock = document.getElementById('edit-zone-block-sequences');
|
||||
const typeLabel = document.getElementById('edit-zone-type-label');
|
||||
const vis = (el, show) => {
|
||||
if (el) el.style.display = show ? '' : 'none';
|
||||
};
|
||||
const k = kind === 'sequences' ? 'sequences' : 'presets';
|
||||
if (typeLabel) typeLabel.style.display = 'none';
|
||||
vis(groupsBlock, true);
|
||||
vis(presetsBlock, k === 'presets');
|
||||
vis(seqBlock, k === 'sequences');
|
||||
vis(presetsBlock, true);
|
||||
vis(seqBlock, true);
|
||||
}
|
||||
|
||||
window.normalizeZoneContentKind = normalizeZoneContentKind;
|
||||
@@ -632,6 +638,52 @@ function renderZonesList(tabs, tabOrder, currentZoneId) {
|
||||
}
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
renderZonesMenuMobile(tabs, tabOrder, currentZoneId);
|
||||
}
|
||||
|
||||
function renderZonesMenuMobile(tabs, tabOrder, currentZoneId) {
|
||||
const dropdown = document.getElementById('zones-menu-dropdown');
|
||||
const menuBtn = document.getElementById('zones-menu-btn');
|
||||
if (!dropdown) return;
|
||||
|
||||
if (!tabOrder || tabOrder.length === 0) {
|
||||
dropdown.innerHTML = '<p class="muted-text zones-menu-empty">No zones</p>';
|
||||
if (menuBtn) menuBtn.textContent = 'Zones';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const zoneId of tabOrder) {
|
||||
const zone = tabs[zoneId];
|
||||
if (!zone) continue;
|
||||
const activeClass = String(zoneId) === String(currentZoneId) ? ' active' : '';
|
||||
const disp = zone.name || `Zone ${zoneId}`;
|
||||
html += `
|
||||
<button type="button" class="zones-menu-item${activeClass}"
|
||||
data-zone-id="${zoneId}" role="menuitem">
|
||||
${escapeHtmlAttr(disp)}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
dropdown.innerHTML = html;
|
||||
|
||||
if (menuBtn) {
|
||||
const cur = tabs[currentZoneId];
|
||||
menuBtn.textContent = cur ? (cur.name || `Zone ${currentZoneId}`) : 'Zones';
|
||||
}
|
||||
}
|
||||
|
||||
function syncZonesMenuSelection(zoneId) {
|
||||
document.querySelectorAll('.zones-menu-item').forEach((item) => {
|
||||
item.classList.toggle('active', item.dataset.zoneId === String(zoneId));
|
||||
});
|
||||
const menuBtn = document.getElementById('zones-menu-btn');
|
||||
const activeItem = document.querySelector(
|
||||
`.zones-menu-item[data-zone-id="${zoneId}"]`,
|
||||
);
|
||||
if (menuBtn && activeItem) {
|
||||
menuBtn.textContent = activeItem.textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Render tabs list in modal (like profiles)
|
||||
@@ -673,14 +725,6 @@ function renderZonesListModal(tabs, tabOrder, currentZoneId) {
|
||||
label.style.color = "#FFD700";
|
||||
}
|
||||
|
||||
const applyButton = document.createElement("button");
|
||||
applyButton.className = "btn btn-secondary btn-small";
|
||||
applyButton.textContent = "Select";
|
||||
applyButton.addEventListener("click", async () => {
|
||||
await selectZone(zoneId);
|
||||
document.getElementById('zones-modal').classList.remove('active');
|
||||
});
|
||||
|
||||
const editButton = document.createElement("button");
|
||||
editButton.className = "btn btn-secondary btn-small";
|
||||
editButton.textContent = "Edit";
|
||||
@@ -771,7 +815,6 @@ function renderZonesListModal(tabs, tabOrder, currentZoneId) {
|
||||
});
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(applyButton);
|
||||
if (editMode) {
|
||||
row.appendChild(editButton);
|
||||
row.appendChild(cloneButton);
|
||||
@@ -819,10 +862,11 @@ async function selectZone(zoneId) {
|
||||
document.querySelectorAll('.zone-button').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
const btn = document.querySelector(`[data-zone-id="${zoneId}"]`);
|
||||
const btn = document.querySelector(`#zones-list .zone-button[data-zone-id="${zoneId}"]`);
|
||||
if (btn) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
syncZonesMenuSelection(zoneId);
|
||||
|
||||
// Set as current zone
|
||||
await setCurrentZone(zoneId);
|
||||
@@ -931,12 +975,6 @@ async function refreshEditTabPresetsUi(zoneId) {
|
||||
return;
|
||||
}
|
||||
const tabData = await tabRes.json();
|
||||
if (!zoneAllowsPresets(tabData, zoneId)) {
|
||||
currentEl.innerHTML =
|
||||
'<span class="muted-text">This zone is for sequences only. Presets are hidden.</span>';
|
||||
addEl.innerHTML = '<span class="muted-text">—</span>';
|
||||
return;
|
||||
}
|
||||
const inTabIds = tabPresetIdsInOrder(tabData);
|
||||
const inTabSet = new Set(inTabIds.map((id) => String(id)));
|
||||
|
||||
@@ -960,12 +998,9 @@ async function refreshEditTabPresetsUi(zoneId) {
|
||||
for (const presetId of inTabIds) {
|
||||
const preset = allPresets[presetId] || {};
|
||||
const name = preset.name || presetId;
|
||||
const block = document.createElement("div");
|
||||
block.style.cssText =
|
||||
"border:1px solid rgba(255,255,255,0.12);border-radius:6px;padding:0.5rem 0.65rem;margin-bottom:0.65rem;";
|
||||
const top = makeRow();
|
||||
const row = makeRow();
|
||||
row.className = "profiles-row edit-zone-item-row";
|
||||
const label = document.createElement("span");
|
||||
label.style.fontWeight = "600";
|
||||
label.textContent = name;
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.type = "button";
|
||||
@@ -977,11 +1012,9 @@ async function refreshEditTabPresetsUi(zoneId) {
|
||||
await window.removePresetFromTab(zoneId, presetId);
|
||||
await refreshEditTabPresetsUi(zoneId);
|
||||
});
|
||||
top.appendChild(label);
|
||||
top.appendChild(removeBtn);
|
||||
block.appendChild(top);
|
||||
|
||||
currentEl.appendChild(block);
|
||||
row.appendChild(label);
|
||||
row.appendChild(removeBtn);
|
||||
currentEl.appendChild(row);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,17 +1102,8 @@ async function openEditZoneModal(zoneId, zone) {
|
||||
});
|
||||
renderZoneGroupsEditor(groupsEditor, window.__editTabGroupRows, groupsMap);
|
||||
|
||||
const kind = effectiveZoneContentKind(tabData);
|
||||
const typeLabel = document.getElementById('edit-zone-type-label');
|
||||
if (typeLabel) {
|
||||
typeLabel.textContent =
|
||||
kind === 'sequences'
|
||||
? 'Zone type: Sequences (set when the zone was created)'
|
||||
: 'Zone type: Presets (set when the zone was created)';
|
||||
}
|
||||
|
||||
if (modal) modal.classList.add("active");
|
||||
applyZoneContentKindEditModal(kind);
|
||||
applyZoneContentKindEditModal();
|
||||
await refreshEditTabPresetsUi(zoneId);
|
||||
if (typeof window.refreshEditTabSequencesUi === "function") {
|
||||
await window.refreshEditTabSequencesUi(zoneId);
|
||||
@@ -1104,7 +1128,6 @@ async function updateZone(zoneId, name, groupRows) {
|
||||
} catch (_) {
|
||||
/* use empty existing */
|
||||
}
|
||||
const lockedKind = effectiveZoneContentKind(existing);
|
||||
const response = await fetch(`/zones/${zoneId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
@@ -1119,7 +1142,6 @@ async function updateZone(zoneId, name, groupRows) {
|
||||
existing.preset_group_ids && typeof existing.preset_group_ids === 'object'
|
||||
? existing.preset_group_ids
|
||||
: {},
|
||||
content_kind: lockedKind,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1131,8 +1153,6 @@ async function updateZone(zoneId, name, groupRows) {
|
||||
if (String(currentZoneId) === String(zoneId)) {
|
||||
await loadZoneContent(zoneId);
|
||||
}
|
||||
// Close modal
|
||||
document.getElementById('edit-zone-modal').classList.remove('active');
|
||||
return true;
|
||||
} else {
|
||||
alert(`Error: ${data.error || 'Failed to update zone'}`);
|
||||
@@ -1145,11 +1165,9 @@ async function updateZone(zoneId, name, groupRows) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new zone (add devices in Edit zone). ``contentKind`` is ``'presets'`` | ``'sequences'``.
|
||||
async function createZone(name, contentKind) {
|
||||
// Create a new zone (add device groups, presets, and sequences in Edit zone).
|
||||
async function createZone(name) {
|
||||
try {
|
||||
const ck =
|
||||
contentKind === 'sequences' || contentKind === 'presets' ? contentKind : 'presets';
|
||||
const response = await fetch('/zones', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -1159,7 +1177,6 @@ async function createZone(name, contentKind) {
|
||||
name: name,
|
||||
names: [],
|
||||
group_ids: [],
|
||||
content_kind: ck,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1196,6 +1213,29 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const newTabNameInput = document.getElementById("new-zone-name");
|
||||
const createZoneButton = document.getElementById("create-zone-btn");
|
||||
|
||||
const zonesMenuBtn = document.getElementById('zones-menu-btn');
|
||||
const zonesMenuDropdown = document.getElementById('zones-menu-dropdown');
|
||||
const mainMenuDropdown = document.getElementById('main-menu-dropdown');
|
||||
|
||||
if (zonesMenuBtn && zonesMenuDropdown) {
|
||||
zonesMenuBtn.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const open = zonesMenuDropdown.classList.toggle('open');
|
||||
zonesMenuBtn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
if (open && mainMenuDropdown) {
|
||||
mainMenuDropdown.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
zonesMenuDropdown.addEventListener('click', async (event) => {
|
||||
const item = event.target.closest('.zones-menu-item');
|
||||
if (!item || !item.dataset.zoneId) return;
|
||||
await selectZone(item.dataset.zoneId);
|
||||
zonesMenuDropdown.classList.remove('open');
|
||||
zonesMenuBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
if (tabsButton && zonesModal) {
|
||||
tabsButton.addEventListener("click", async () => {
|
||||
zonesModal.classList.add("active");
|
||||
@@ -1240,12 +1280,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const name = newTabNameInput.value.trim();
|
||||
|
||||
if (name) {
|
||||
const kindRadio = document.querySelector(
|
||||
'input[name="new-zone-content-kind"]:checked',
|
||||
);
|
||||
const contentKind =
|
||||
kindRadio && kindRadio.value === 'sequences' ? 'sequences' : 'presets';
|
||||
await createZone(name, contentKind);
|
||||
await createZone(name);
|
||||
if (newTabNameInput) newTabNameInput.value = "";
|
||||
}
|
||||
};
|
||||
@@ -1276,7 +1311,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
if (zoneId && name) {
|
||||
await updateZone(zoneId, name, groupRows);
|
||||
editZoneForm.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 + Wi‑Fi 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 Wi‑Fi defaults once per group, then attach groups to a zone for standalone presets (sequences use each lane’s 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;">Wi‑Fi 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 & 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, Wi‑Fi 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 Wi‑Fi 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">✓ 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 Wi‑Fi 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 Wi‑Fi defaults once per group, then attach groups to a zone.</li>
|
||||
<li>Standalone presets use the zone’s 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>n1–n8</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 20–200 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 10–80 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>n1–n8</strong>.</li>
|
||||
<li>Wi‑Fi 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 profile’s 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 Wi‑Fi.</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>
|
||||
|
||||
|
||||