Refresh tabs/presets UI and add a mobile menu.

This improves navigation and profile workflows on smaller screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 13:51:09 +13:00
parent 6c6ed22dbe
commit d907ca37ad
6 changed files with 917 additions and 379 deletions

View File

@@ -684,18 +684,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Handle "Add Preset" button in tab area (dynamically loaded)
document.addEventListener('click', async (e) => {
if (e.target && e.target.id === 'preset-add-btn-tab') {
await showAddPresetToTabModal();
const showAddPresetToTabModal = async (optionalTabId) => {
let tabId = optionalTabId;
if (!tabId) {
// Get current tab ID from the presets section
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
tabId = leftPanel ? leftPanel.dataset.tabId : null;
}
});
const showAddPresetToTabModal = async () => {
// Get current tab ID from the left-panel
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
let tabId = leftPanel ? leftPanel.dataset.tabId : null;
if (!tabId) {
// Fallback: try to get from URL
const pathParts = window.location.pathname.split('/');
@@ -704,7 +699,6 @@ document.addEventListener('DOMContentLoaded', () => {
tabId = pathParts[tabIndex + 1];
}
}
if (!tabId) {
alert('Could not determine current tab.');
return;
@@ -761,13 +755,12 @@ document.addEventListener('DOMContentLoaded', () => {
const listContainer = document.getElementById('add-preset-list');
const presetNames = Object.keys(allPresets);
if (presetNames.length === 0) {
listContainer.innerHTML = '<p class="muted-text">No presets available. Create a preset first.</p>';
const availableToAdd = presetNames.filter(presetId => !currentTabPresets.includes(presetId));
if (availableToAdd.length === 0) {
listContainer.innerHTML = '<p class="muted-text">No presets to add. All presets are already in this tab, or create a preset first.</p>';
} else {
presetNames.forEach(presetId => {
availableToAdd.forEach(presetId => {
const preset = allPresets[presetId];
const isInCurrentTab = currentTabPresets.includes(presetId);
const row = document.createElement('div');
row.className = 'profiles-row';
@@ -779,31 +772,17 @@ document.addEventListener('DOMContentLoaded', () => {
details.style.fontSize = '0.85em';
details.textContent = preset.pattern || '-';
const actionButton = document.createElement('button');
if (isInCurrentTab) {
// Already in this tab: allow removing from this tab
actionButton.className = 'btn btn-danger btn-small';
actionButton.textContent = 'Remove';
actionButton.addEventListener('click', async (e) => {
e.stopPropagation();
if (confirm(`Remove preset "${preset.name || presetId}" from this tab?`)) {
await removePresetFromTab(tabId, presetId);
modal.remove();
}
});
} else {
// Not yet in this tab: allow adding (even if used in other tabs)
actionButton.className = 'btn btn-primary btn-small';
actionButton.textContent = 'Add';
actionButton.addEventListener('click', async () => {
await addPresetToTab(presetId, tabId);
modal.remove();
});
}
const addButton = document.createElement('button');
addButton.className = 'btn btn-primary btn-small';
addButton.textContent = 'Add';
addButton.addEventListener('click', async () => {
await addPresetToTab(presetId, tabId);
modal.remove();
});
row.appendChild(label);
row.appendChild(details);
row.appendChild(actionButton);
row.appendChild(addButton);
listContainer.appendChild(row);
});
}
@@ -824,6 +803,9 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Failed to load presets.');
}
};
try {
window.showAddPresetToTabModal = showAddPresetToTabModal;
} catch (e) {}
const addPresetToTab = async (presetId, tabId) => {
if (!tabId) {
@@ -906,6 +888,9 @@ document.addEventListener('DOMContentLoaded', () => {
alert('Failed to add preset to tab.');
}
};
try {
window.addPresetToTab = addPresetToTab;
} catch (e) {}
if (presetEditorCloseButton) {
presetEditorCloseButton.addEventListener('click', closeEditor);
}
@@ -1126,7 +1111,8 @@ document.addEventListener('DOMContentLoaded', () => {
// Build an ESPNow preset message for a single preset and optionally include a select
// for the given device names, then send it via WebSocket.
const sendPresetViaEspNow = (presetId, preset, deviceNames) => {
// saveToDevice defaults to true.
const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true) => {
try {
const colors = Array.isArray(preset.colors) && preset.colors.length
? preset.colors
@@ -1152,6 +1138,10 @@ const sendPresetViaEspNow = (presetId, preset, deviceNames) => {
},
},
};
if (saveToDevice) {
// Instruct led-driver to save this preset when received.
message.save = true;
}
// Optionally include a select section for specific devices
if (Array.isArray(deviceNames) && deviceNames.length > 0) {
@@ -1225,11 +1215,6 @@ const ensurePresetContextMenu = () => {
cursor: pointer;
font-size: 0.9rem;
`;
if (action === 'remove') {
// Visually emphasize and align remove to the right
item.style.textAlign = 'right';
item.style.color = '#ff8080';
}
item.addEventListener('mouseover', () => {
item.style.backgroundColor = '#3a3a3a';
});
@@ -1240,20 +1225,17 @@ const ensurePresetContextMenu = () => {
};
addItem('Edit preset…', 'edit');
addItem('Remove', 'remove');
menu.addEventListener('click', async (e) => {
const btn = e.target.closest('button[data-action]');
if (!btn || !presetContextTarget) {
return;
}
const { tabId, presetId } = presetContextTarget;
const { presetId } = presetContextTarget;
const action = btn.dataset.action;
hidePresetContextMenu();
if (action === 'edit') {
await editPresetFromTab(presetId);
} else if (action === 'remove') {
await removePresetFromTab(tabId, presetId);
}
});
@@ -1361,24 +1343,24 @@ const savePresetGrid = async (tabId, presetGrid) => {
}
};
// Function to get drop target in 2D grid
// Function to get drop target: the cell that contains the cursor (or closest if in a gap)
const getDropTarget = (container, x, y) => {
const draggableElements = [...container.querySelectorAll('.draggable-preset:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
// First try: find the element whose rect contains the cursor
const containing = draggableElements.find((child) => {
const box = child.getBoundingClientRect();
const centerX = box.left + box.width / 2;
const centerY = box.top + box.height / 2;
const distanceX = Math.abs(x - centerX);
const distanceY = Math.abs(y - centerY);
const distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
if (distance < closest.distance) {
return { distance: distance, element: child };
} else {
return closest;
}
}, { distance: Infinity }).element;
return x >= box.left && x <= box.right && y >= box.top && y <= box.bottom;
});
if (containing) return containing;
// Fallback: closest element by distance to center
const closest = draggableElements.reduce((best, child) => {
const box = child.getBoundingClientRect();
const cx = box.left + box.width / 2;
const cy = box.top + box.height / 2;
const d = Math.hypot(x - cx, y - cy);
return d < best.distance ? { distance: d, element: child } : best;
}, { distance: Infinity });
return closest.element;
};
// Function to render presets for a specific tab in 2D grid
@@ -1426,17 +1408,8 @@ const renderTabPresets = async (tabId) => {
const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
if (dropTarget && dropTarget !== dragging) {
// Insert before or after based on position
const rect = dropTarget.getBoundingClientRect();
const draggingRect = dragging.getBoundingClientRect();
if (e.clientX < rect.left + rect.width / 2) {
// Insert before
presetsList.insertBefore(dragging, dropTarget);
} else {
// Insert after
presetsList.insertBefore(dragging, dropTarget.nextSibling);
}
// Insert before drop target so the dragged item takes that cell's position
presetsList.insertBefore(dragging, dropTarget);
}
});
@@ -1477,7 +1450,7 @@ const renderTabPresets = async (tabId) => {
const empty = document.createElement('p');
empty.className = 'muted-text';
empty.style.gridColumn = '1 / -1'; // Span all columns
empty.textContent = 'No presets added to this tab. Click "Add Preset" to add one.';
empty.textContent = 'No presets added to this tab. Open the tab\'s Edit menu and click "Add Preset" to add one.';
presetsList.appendChild(empty);
} else {
flatPresets.forEach((presetId) => {
@@ -1496,97 +1469,67 @@ const renderTabPresets = async (tabId) => {
};
const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
// Create wrapper div for button and edit button
const wrapper = document.createElement('div');
wrapper.style.cssText = 'display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem;';
wrapper.draggable = true;
wrapper.dataset.presetId = presetId;
wrapper.classList.add('draggable-preset');
// Create preset button
const button = document.createElement('button');
button.className = 'pattern-button';
button.style.flex = '1';
button.className = 'pattern-button draggable-preset';
button.draggable = true;
button.dataset.presetId = presetId;
if (isSelected) {
button.classList.add('active');
}
button.dataset.presetId = presetId;
const presetInfo = document.createElement('div');
presetInfo.style.cssText = 'display: flex; flex-direction: column; align-items: flex-start; width: 100%;';
const colors = Array.isArray(preset.colors) ? preset.colors.filter(c => c) : [];
const isRainbow = (preset.pattern || '').toLowerCase() === 'rainbow';
const barColors = isRainbow
? ['#FF0000', '#FF7F00', '#FFFF00', '#00FF00', '#0000FF', '#4B0082', '#8F00FF']
: colors;
if (barColors.length > 0) {
const n = barColors.length;
const stops = barColors.flatMap((c, i) => {
const start = (100 * i / n).toFixed(2);
const end = (100 * (i + 1) / n).toFixed(2);
return [`${c} ${start}%`, `${c} ${end}%`];
}).join(', ');
button.style.backgroundImage = `linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), linear-gradient(to right, ${stops})`;
}
const presetNameLabel = document.createElement('span');
presetNameLabel.textContent = preset.name || presetId;
presetNameLabel.style.fontWeight = 'bold';
presetNameLabel.style.marginBottom = '0.25rem';
const presetDetails = document.createElement('span');
presetDetails.style.fontSize = '0.85em';
presetDetails.style.color = '#aaa';
const colors = Array.isArray(preset.colors) ? preset.colors : [];
presetDetails.textContent = `${preset.pattern || '-'}${colors.length} color${colors.length !== 1 ? 's' : ''}`;
presetInfo.appendChild(presetNameLabel);
presetInfo.appendChild(presetDetails);
button.appendChild(presetInfo);
// Left-click selects preset, right-click opens editor
button.addEventListener('click', (e) => {
if (isDraggingPreset) {
return;
}
presetNameLabel.className = 'pattern-button-label';
button.appendChild(presetNameLabel);
// Remove active class from all presets in this tab
button.addEventListener('click', (e) => {
if (isDraggingPreset) return;
const presetsList = document.getElementById('presets-list-tab');
if (presetsList) {
presetsList.querySelectorAll('.pattern-button').forEach(btn => {
btn.classList.remove('active');
});
presetsList.querySelectorAll('.pattern-button').forEach(btn => btn.classList.remove('active'));
}
// Add active class to clicked preset
button.classList.add('active');
// Store selected preset for this tab
selectedPresets[tabId] = presetId;
// Build and send a select message via WebSocket for all device names in this tab.
const section = button.closest('.presets-section');
sendSelectForCurrentTabDevices(presetId, section);
});
button.addEventListener('contextmenu', async (e) => {
e.preventDefault();
if (isDraggingPreset) {
return;
}
// Right-click: directly open the preset editor using data we already have
if (isDraggingPreset) return;
await editPresetFromTab(presetId, tabId, preset);
});
wrapper.appendChild(button);
// Add drag event handlers
wrapper.addEventListener('dragstart', (e) => {
button.addEventListener('dragstart', (e) => {
isDraggingPreset = true;
wrapper.classList.add('dragging');
button.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', presetId);
});
wrapper.addEventListener('dragend', (e) => {
wrapper.classList.remove('dragging');
// Remove any drag-over classes from siblings
document.querySelectorAll('.draggable-preset').forEach(el => {
el.classList.remove('drag-over');
});
// Reset dragging flag after a short delay to allow click event to check it
setTimeout(() => {
isDraggingPreset = false;
}, 100);
button.addEventListener('dragend', (e) => {
button.classList.remove('dragging');
document.querySelectorAll('.draggable-preset').forEach(el => el.classList.remove('drag-over'));
setTimeout(() => { isDraggingPreset = false; }, 100);
});
return wrapper;
return button;
};
const editPresetFromTab = async (presetId, tabId, existingPreset) => {
@@ -1685,6 +1628,9 @@ const removePresetFromTab = async (tabId, presetId) => {
alert('Failed to remove preset from tab.');
}
};
try {
window.removePresetFromTab = removePresetFromTab;
} catch (e) {}
// Listen for HTMX swaps to render presets
document.body.addEventListener('htmx:afterSwap', (event) => {