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:
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user