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

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

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

View File

@@ -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', () => {