feat(ui): add run/edit workflow and improve preset color editing
Made-with: Cursor
This commit is contained in:
@@ -4,7 +4,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const closeButton = document.getElementById('color-palette-close-btn');
|
||||
const paletteContainer = document.getElementById('palette-container');
|
||||
const paletteNewColor = document.getElementById('palette-new-color');
|
||||
const paletteAddButton = document.getElementById('palette-add-color-btn');
|
||||
const profileNameDisplay = document.getElementById('palette-current-profile-name');
|
||||
|
||||
if (!paletteButton || !paletteModal || !paletteContainer) {
|
||||
@@ -177,8 +176,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', closeModal);
|
||||
}
|
||||
if (paletteAddButton && paletteNewColor) {
|
||||
paletteAddButton.addEventListener('click', async () => {
|
||||
if (paletteNewColor) {
|
||||
const addSelectedColor = async () => {
|
||||
const color = paletteNewColor.value;
|
||||
if (!color) {
|
||||
return;
|
||||
@@ -188,7 +187,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return;
|
||||
}
|
||||
await savePalette([...currentPalette, color]);
|
||||
});
|
||||
};
|
||||
// Add when the picker closes (user confirms selection).
|
||||
paletteNewColor.addEventListener('change', addSelectedColor);
|
||||
}
|
||||
paletteModal.addEventListener('click', (event) => {
|
||||
if (event.target === paletteModal) {
|
||||
|
||||
+423
-214
@@ -2,6 +2,72 @@
|
||||
let espnowSocket = null;
|
||||
let espnowSocketReady = false;
|
||||
let espnowPendingMessages = [];
|
||||
let currentProfileIdCache = null;
|
||||
|
||||
const getCurrentProfileId = async () => {
|
||||
try {
|
||||
const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return currentProfileIdCache ? String(currentProfileIdCache) : null;
|
||||
const data = await res.json();
|
||||
const id = data && (data.id || (data.profile && data.profile.id));
|
||||
currentProfileIdCache = id ? String(id) : null;
|
||||
return currentProfileIdCache;
|
||||
} catch (_) {
|
||||
return currentProfileIdCache ? String(currentProfileIdCache) : null;
|
||||
}
|
||||
};
|
||||
|
||||
const filterPresetsForCurrentProfile = async (presetsObj) => {
|
||||
const scoped = presetsObj && typeof presetsObj === 'object' ? presetsObj : {};
|
||||
const currentProfileId = await getCurrentProfileId();
|
||||
if (!currentProfileId) return scoped;
|
||||
return Object.fromEntries(
|
||||
Object.entries(scoped).filter(([, preset]) => {
|
||||
if (!preset || typeof preset !== 'object') return false;
|
||||
if (!('profile_id' in preset)) return true; // Legacy records
|
||||
return String(preset.profile_id) === String(currentProfileId);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getCurrentProfileData = async () => {
|
||||
try {
|
||||
const res = await fetch('/profiles/current', { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getCurrentProfilePaletteColors = async () => {
|
||||
const profileData = await getCurrentProfileData();
|
||||
const profile = profileData && profileData.profile;
|
||||
const paletteId = profile && (profile.palette_id || profile.paletteId);
|
||||
if (!paletteId) return [];
|
||||
try {
|
||||
const res = await fetch(`/palettes/${paletteId}`, { headers: { Accept: 'application/json' } });
|
||||
if (!res.ok) return [];
|
||||
const pal = await res.json();
|
||||
return Array.isArray(pal.colors) ? pal.colors : [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const resolveColorsWithPaletteRefs = (colors, paletteRefs, paletteColors) => {
|
||||
const baseColors = Array.isArray(colors) ? colors : [];
|
||||
const refs = Array.isArray(paletteRefs) ? paletteRefs : [];
|
||||
const pal = Array.isArray(paletteColors) ? paletteColors : [];
|
||||
return baseColors.map((color, idx) => {
|
||||
const refRaw = refs[idx];
|
||||
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
||||
if (Number.isInteger(ref) && ref >= 0 && ref < pal.length && pal[ref]) {
|
||||
return pal[ref];
|
||||
}
|
||||
return color;
|
||||
});
|
||||
};
|
||||
|
||||
const getEspnowSocket = () => {
|
||||
if (espnowSocket && (espnowSocket.readyState === WebSocket.OPEN || espnowSocket.readyState === WebSocket.CONNECTING)) {
|
||||
@@ -105,7 +171,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const presetPatternInput = document.getElementById('preset-pattern-input');
|
||||
const presetColorsContainer = document.getElementById('preset-colors-container');
|
||||
const presetNewColorInput = document.getElementById('preset-new-color');
|
||||
const presetAddColorButton = document.getElementById('preset-add-color-btn');
|
||||
const presetBrightnessInput = document.getElementById('preset-brightness-input');
|
||||
const presetDelayInput = document.getElementById('preset-delay-input');
|
||||
const presetDefaultButton = document.getElementById('preset-default-btn');
|
||||
@@ -123,6 +188,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
let cachedPresets = {};
|
||||
let cachedPatterns = {};
|
||||
let currentPresetColors = []; // Track colors for the current preset
|
||||
let currentPresetPaletteRefs = []; // Palette index refs per color (null for direct colors)
|
||||
|
||||
// Function to get max colors for current pattern
|
||||
const getMaxColors = () => {
|
||||
@@ -158,7 +224,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
// Hide/show the actions (color picker and buttons)
|
||||
const colorActions = presetColorsContainer.nextElementSibling;
|
||||
if (colorActions && (colorActions.querySelector('#preset-add-color-btn') || colorActions.querySelector('#preset-new-color'))) {
|
||||
if (colorActions && colorActions.querySelector('#preset-new-color')) {
|
||||
colorActions.style.display = shouldShow ? '' : 'none';
|
||||
}
|
||||
}
|
||||
@@ -172,11 +238,24 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
return parseInt(input.value, 10) || 0;
|
||||
};
|
||||
|
||||
const renderPresetColors = (colors) => {
|
||||
const renderPresetColors = (colors, paletteRefs) => {
|
||||
if (!presetColorsContainer) return;
|
||||
|
||||
presetColorsContainer.innerHTML = '';
|
||||
currentPresetColors = colors || [];
|
||||
currentPresetColors = Array.isArray(colors) ? colors.slice() : [];
|
||||
if (Array.isArray(paletteRefs)) {
|
||||
currentPresetPaletteRefs = currentPresetColors.map((_, i) => {
|
||||
const refRaw = paletteRefs[i];
|
||||
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
||||
return Number.isInteger(ref) ? ref : null;
|
||||
});
|
||||
} else {
|
||||
currentPresetPaletteRefs = currentPresetColors.map((_, i) => {
|
||||
const refRaw = currentPresetPaletteRefs[i];
|
||||
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
||||
return Number.isInteger(ref) ? ref : null;
|
||||
});
|
||||
}
|
||||
|
||||
// Get max colors for current pattern
|
||||
const maxColors = getMaxColors();
|
||||
@@ -185,7 +264,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (currentPresetColors.length === 0) {
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.textContent = `No colors added. Click "Add Color" to add colors.${maxColorsText}`;
|
||||
empty.textContent = `No colors added. Use the color picker to add colors.${maxColorsText}`;
|
||||
presetColorsContainer.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
@@ -208,6 +287,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
swatchWrapper.style.cssText = 'position: relative; display: inline-block;';
|
||||
swatchWrapper.draggable = true;
|
||||
swatchWrapper.dataset.colorIndex = index;
|
||||
const refAtIndex = currentPresetPaletteRefs[index];
|
||||
swatchWrapper.dataset.paletteRef = Number.isInteger(refAtIndex) ? String(refAtIndex) : '';
|
||||
swatchWrapper.classList.add('draggable-color-swatch');
|
||||
|
||||
const swatch = document.createElement('div');
|
||||
@@ -223,6 +304,31 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
`;
|
||||
swatch.title = `${color} - Drag to reorder`;
|
||||
|
||||
if (Number.isInteger(refAtIndex)) {
|
||||
const linkedBadge = document.createElement('span');
|
||||
linkedBadge.textContent = 'P';
|
||||
linkedBadge.title = `Linked to palette color #${refAtIndex + 1}`;
|
||||
linkedBadge.style.cssText = `
|
||||
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;
|
||||
z-index: 11;
|
||||
border: 1px solid rgba(255,255,255,0.35);
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.35);
|
||||
`;
|
||||
swatchWrapper.appendChild(linkedBadge);
|
||||
}
|
||||
|
||||
// Color picker overlay
|
||||
const colorPicker = document.createElement('input');
|
||||
colorPicker.type = 'color';
|
||||
@@ -239,7 +345,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
`;
|
||||
colorPicker.addEventListener('change', (e) => {
|
||||
currentPresetColors[index] = e.target.value;
|
||||
renderPresetColors(currentPresetColors);
|
||||
// Manual picker edit breaks palette linkage for this slot.
|
||||
currentPresetPaletteRefs[index] = null;
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
});
|
||||
// Prevent color picker from interfering with drag
|
||||
colorPicker.addEventListener('mousedown', (e) => {
|
||||
@@ -271,7 +379,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
removeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
currentPresetColors.splice(index, 1);
|
||||
renderPresetColors(currentPresetColors);
|
||||
currentPresetPaletteRefs.splice(index, 1);
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
});
|
||||
// Prevent remove button from interfering with drag
|
||||
removeBtn.addEventListener('mousedown', (e) => {
|
||||
@@ -326,12 +435,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const colorPicker = el.querySelector('input[type="color"]');
|
||||
return colorPicker ? colorPicker.value : null;
|
||||
}).filter(color => color !== null);
|
||||
const newRefOrder = colorElements.map((el) => {
|
||||
const refRaw = el.dataset.paletteRef;
|
||||
const ref = Number.isInteger(refRaw) ? refRaw : parseInt(refRaw, 10);
|
||||
return Number.isInteger(ref) ? ref : null;
|
||||
});
|
||||
|
||||
// Update current colors array
|
||||
currentPresetColors = newColorOrder;
|
||||
currentPresetPaletteRefs = newRefOrder;
|
||||
|
||||
// Re-render to update indices
|
||||
renderPresetColors(currentPresetColors);
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
});
|
||||
|
||||
presetColorsContainer.appendChild(swatchContainer);
|
||||
@@ -361,7 +476,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const patternName = preset.pattern || '';
|
||||
presetPatternInput.value = patternName;
|
||||
const colors = Array.isArray(preset.colors) ? preset.colors : [];
|
||||
renderPresetColors(colors);
|
||||
const paletteRefs = Array.isArray(preset.palette_refs) ? preset.palette_refs : [];
|
||||
renderPresetColors(colors, paletteRefs);
|
||||
presetBrightnessInput.value = preset.brightness || 0;
|
||||
presetDelayInput.value = preset.delay || 0;
|
||||
|
||||
@@ -424,6 +540,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
currentEditId = null;
|
||||
currentEditTabId = null;
|
||||
currentPresetColors = [];
|
||||
currentPresetPaletteRefs = [];
|
||||
setFormValues({
|
||||
name: '',
|
||||
pattern: '',
|
||||
@@ -505,6 +622,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
name: presetNameInput ? presetNameInput.value.trim() : '',
|
||||
pattern: presetPatternInput ? presetPatternInput.value.trim() : '',
|
||||
colors: currentPresetColors || [],
|
||||
palette_refs: currentPresetPaletteRefs || [],
|
||||
// Use canonical field names expected by the device / API
|
||||
brightness: presetBrightnessInput ? parseInt(presetBrightnessInput.value, 10) || 0 : 0,
|
||||
delay: presetDelayInput ? parseInt(presetDelayInput.value, 10) || 0 : 0,
|
||||
@@ -633,9 +751,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const editButton = document.createElement('button');
|
||||
editButton.className = 'btn btn-secondary btn-small';
|
||||
editButton.textContent = 'Edit';
|
||||
editButton.addEventListener('click', () => {
|
||||
editButton.addEventListener('click', async () => {
|
||||
currentEditId = presetId;
|
||||
setFormValues(preset || {});
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
const presetForEditor = {
|
||||
...(preset || {}),
|
||||
colors: resolveColorsWithPaletteRefs(
|
||||
(preset && preset.colors) || [],
|
||||
(preset && preset.palette_refs) || [],
|
||||
paletteColors,
|
||||
),
|
||||
};
|
||||
setFormValues(presetForEditor);
|
||||
openEditor();
|
||||
});
|
||||
|
||||
@@ -698,7 +825,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
throw new Error('Failed to load presets');
|
||||
}
|
||||
const presets = await response.json();
|
||||
renderPresets(presets);
|
||||
const filtered = await filterPresetsForCurrentProfile(presets);
|
||||
renderPresets(filtered);
|
||||
} catch (error) {
|
||||
console.error('Load presets failed:', error);
|
||||
presetsList.innerHTML = '';
|
||||
@@ -757,7 +885,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load presets');
|
||||
}
|
||||
const allPresets = await response.json();
|
||||
const allPresetsRaw = await response.json();
|
||||
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
|
||||
|
||||
// Load only the current tab's presets so we can avoid duplicates within this tab.
|
||||
let currentTabPresets = [];
|
||||
@@ -946,12 +1075,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// Update color section visibility
|
||||
updateColorSectionVisibility();
|
||||
// Re-render colors to show updated max colors limit
|
||||
renderPresetColors(currentPresetColors);
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
});
|
||||
}
|
||||
// Add Color button handler
|
||||
if (presetAddColorButton && presetNewColorInput) {
|
||||
presetAddColorButton.addEventListener('click', () => {
|
||||
// Color picker auto-add handler
|
||||
if (presetNewColorInput) {
|
||||
const tryAddSelectedColor = () => {
|
||||
const color = presetNewColorInput.value;
|
||||
if (!color) return;
|
||||
|
||||
@@ -967,60 +1096,86 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
currentPresetColors.push(color);
|
||||
renderPresetColors(currentPresetColors);
|
||||
});
|
||||
currentPresetPaletteRefs.push(null);
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
};
|
||||
// Add when the picker closes (user confirms selection).
|
||||
presetNewColorInput.addEventListener('change', tryAddSelectedColor);
|
||||
}
|
||||
|
||||
// Add from Palette button handler
|
||||
if (presetAddFromPaletteButton) {
|
||||
presetAddFromPaletteButton.addEventListener('click', () => {
|
||||
const openButton = document.getElementById('color-palette-btn');
|
||||
if (openButton) {
|
||||
openButton.click();
|
||||
}
|
||||
const modal = document.getElementById('color-palette-modal');
|
||||
const modalList = document.getElementById('palette-container');
|
||||
if (modal) {
|
||||
modal.classList.add('active');
|
||||
}
|
||||
if (!modalList) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePick = (event) => {
|
||||
const row = event.target.closest('[data-color]');
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
const picked = row.dataset.color;
|
||||
if (!picked) {
|
||||
presetAddFromPaletteButton.addEventListener('click', async () => {
|
||||
try {
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
if (!Array.isArray(paletteColors) || paletteColors.length === 0) {
|
||||
alert('No profile palette colors available.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPresetColors.includes(picked)) {
|
||||
alert('This color is already in the list.');
|
||||
return;
|
||||
}
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
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>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const maxColors = getMaxColors();
|
||||
if (currentPresetColors.length >= maxColors) {
|
||||
alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
const list = modal.querySelector('#pick-palette-list');
|
||||
paletteColors.forEach((color, idx) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profiles-row';
|
||||
row.style.display = 'flex';
|
||||
row.style.alignItems = 'center';
|
||||
row.style.gap = '0.75rem';
|
||||
row.dataset.paletteIndex = String(idx);
|
||||
row.dataset.paletteColor = color;
|
||||
row.innerHTML = `
|
||||
<div style="width:28px;height:28px;border-radius:4px;border:1px solid #555;background:${color};"></div>
|
||||
<span style="flex:1">${color}</span>
|
||||
<button class="btn btn-primary btn-small" type="button">Use</button>
|
||||
`;
|
||||
list.appendChild(row);
|
||||
});
|
||||
|
||||
const close = () => modal.remove();
|
||||
modal.querySelector('#pick-palette-close-btn').addEventListener('click', close);
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) close();
|
||||
});
|
||||
|
||||
list.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('button');
|
||||
if (!btn) return;
|
||||
const row = e.target.closest('[data-palette-index]');
|
||||
if (!row) return;
|
||||
const color = row.dataset.paletteColor;
|
||||
const ref = parseInt(row.dataset.paletteIndex, 10);
|
||||
if (!color || !Number.isInteger(ref)) return;
|
||||
|
||||
if (currentPresetColors.includes(color) && currentPresetPaletteRefs.includes(ref)) {
|
||||
alert('That palette color is already linked.');
|
||||
return;
|
||||
}
|
||||
const maxColors = getMaxColors();
|
||||
if (currentPresetColors.length >= maxColors) {
|
||||
alert(`This pattern allows a maximum of ${maxColors} color${maxColors !== 1 ? 's' : ''}.`);
|
||||
return;
|
||||
}
|
||||
modalList.removeEventListener('click', handlePick);
|
||||
return;
|
||||
}
|
||||
|
||||
currentPresetColors.push(picked);
|
||||
renderPresetColors(currentPresetColors);
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
}
|
||||
modalList.removeEventListener('click', handlePick);
|
||||
};
|
||||
|
||||
modalList.addEventListener('click', handlePick);
|
||||
currentPresetColors.push(color);
|
||||
currentPresetPaletteRefs.push(ref);
|
||||
renderPresetColors(currentPresetColors, currentPresetPaletteRefs);
|
||||
close();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to add from palette:', err);
|
||||
alert('Failed to load palette colors.');
|
||||
}
|
||||
});
|
||||
}
|
||||
const presetSendButton = document.getElementById('preset-send-btn');
|
||||
@@ -1136,7 +1291,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
currentEditId = presetId;
|
||||
currentEditTabId = tabId || null;
|
||||
await loadPatterns();
|
||||
setFormValues(preset);
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
setFormValues({
|
||||
...(preset || {}),
|
||||
colors: resolveColorsWithPaletteRefs(
|
||||
(preset && preset.colors) || [],
|
||||
(preset && preset.palette_refs) || [],
|
||||
paletteColors,
|
||||
),
|
||||
});
|
||||
openEditor();
|
||||
});
|
||||
|
||||
@@ -1175,11 +1338,13 @@ 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.
|
||||
// saveToDevice defaults to true.
|
||||
const sendPresetViaEspNow = (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
|
||||
const sendPresetViaEspNow = async (presetId, preset, deviceNames, saveToDevice = true, setDefault = false) => {
|
||||
try {
|
||||
const colors = Array.isArray(preset.colors) && preset.colors.length
|
||||
const baseColors = Array.isArray(preset.colors) && preset.colors.length
|
||||
? preset.colors
|
||||
: ['#FFFFFF'];
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
const colors = resolveColorsWithPaletteRefs(baseColors, preset.palette_refs, paletteColors);
|
||||
|
||||
const message = {
|
||||
v: '1',
|
||||
@@ -1261,97 +1426,29 @@ try {
|
||||
|
||||
// Store selected preset per tab
|
||||
const selectedPresets = {};
|
||||
// Run vs Edit for tab preset strip (in-memory only — each full page load starts in run mode)
|
||||
let presetUiMode = 'run';
|
||||
|
||||
const getPresetUiMode = () => (presetUiMode === 'edit' ? 'edit' : 'run');
|
||||
|
||||
const setPresetUiMode = (mode) => {
|
||||
presetUiMode = mode === 'edit' ? 'edit' : 'run';
|
||||
};
|
||||
|
||||
const updateUiModeToggleButtons = () => {
|
||||
const mode = getPresetUiMode();
|
||||
// Label is the mode you switch *to* (opposite of current)
|
||||
const label = mode === 'edit' ? 'Run mode' : 'Edit mode';
|
||||
document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
|
||||
btn.textContent = label;
|
||||
btn.setAttribute('aria-pressed', mode === 'edit' ? 'true' : 'false');
|
||||
btn.classList.toggle('ui-mode-toggle--edit', mode === 'edit');
|
||||
});
|
||||
document.body.classList.toggle('preset-ui-edit', mode === 'edit');
|
||||
document.body.classList.toggle('preset-ui-run', mode === 'run');
|
||||
};
|
||||
// Track if we're currently dragging a preset
|
||||
let isDraggingPreset = false;
|
||||
// Context menu for tab presets
|
||||
let presetContextMenu = null;
|
||||
let presetContextTarget = null;
|
||||
|
||||
const ensurePresetContextMenu = () => {
|
||||
if (presetContextMenu) {
|
||||
return presetContextMenu;
|
||||
}
|
||||
const menu = document.createElement('div');
|
||||
menu.id = 'preset-context-menu';
|
||||
menu.style.cssText = `
|
||||
position: fixed;
|
||||
z-index: 2000;
|
||||
background: #2e2e2e;
|
||||
border: 1px solid #4a4a4a;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.6);
|
||||
padding: 0.25rem 0;
|
||||
min-width: 160px;
|
||||
display: none;
|
||||
`;
|
||||
|
||||
const addItem = (label, action) => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.textContent = label;
|
||||
item.dataset.action = action;
|
||||
item.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.4rem 0.75rem;
|
||||
background: transparent;
|
||||
color: #eee;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
`;
|
||||
item.addEventListener('mouseover', () => {
|
||||
item.style.backgroundColor = '#3a3a3a';
|
||||
});
|
||||
item.addEventListener('mouseout', () => {
|
||||
item.style.backgroundColor = 'transparent';
|
||||
});
|
||||
menu.appendChild(item);
|
||||
};
|
||||
|
||||
addItem('Edit preset…', 'edit');
|
||||
|
||||
menu.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('button[data-action]');
|
||||
if (!btn || !presetContextTarget) {
|
||||
return;
|
||||
}
|
||||
const { presetId } = presetContextTarget;
|
||||
const action = btn.dataset.action;
|
||||
hidePresetContextMenu();
|
||||
if (action === 'edit') {
|
||||
await editPresetFromTab(presetId);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(menu);
|
||||
presetContextMenu = menu;
|
||||
|
||||
// Hide on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!presetContextMenu) return;
|
||||
if (e.target.closest('#preset-context-menu')) return;
|
||||
hidePresetContextMenu();
|
||||
});
|
||||
|
||||
return menu;
|
||||
};
|
||||
|
||||
const showPresetContextMenu = (x, y, tabId, presetId, preset) => {
|
||||
const menu = ensurePresetContextMenu();
|
||||
presetContextTarget = { tabId, presetId, preset };
|
||||
menu.style.left = `${x}px`;
|
||||
menu.style.top = `${y}px`;
|
||||
menu.style.display = 'block';
|
||||
};
|
||||
|
||||
const hidePresetContextMenu = () => {
|
||||
if (presetContextMenu) {
|
||||
presetContextMenu.style.display = 'none';
|
||||
}
|
||||
presetContextTarget = null;
|
||||
};
|
||||
|
||||
// Function to convert 2D grid to flat array (for backward compatibility)
|
||||
const gridToArray = (presetsGrid) => {
|
||||
@@ -1449,6 +1546,25 @@ const getDropTarget = (container, x, y) => {
|
||||
return closest.element;
|
||||
};
|
||||
|
||||
/**
|
||||
* Move dragged tile onto the drop target's slot.
|
||||
* When moving down the list (fromIdx < toIdx), insertBefore(dragging, dropTarget) lands one index
|
||||
* too early; use the next element sibling so the item occupies the target slot.
|
||||
*/
|
||||
const insertDraggingOntoTarget = (presetsList, dragging, dropTarget) => {
|
||||
const siblings = [...presetsList.querySelectorAll('.draggable-preset')];
|
||||
const fromIdx = siblings.indexOf(dragging);
|
||||
const toIdx = siblings.indexOf(dropTarget);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
|
||||
if (fromIdx < toIdx) {
|
||||
const next = dropTarget.nextElementSibling;
|
||||
presetsList.insertBefore(dragging, next);
|
||||
} else {
|
||||
presetsList.insertBefore(dragging, dropTarget);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to render presets for a specific tab in 2D grid
|
||||
const renderTabPresets = async (tabId) => {
|
||||
const presetsList = document.getElementById('presets-list-tab');
|
||||
@@ -1482,47 +1598,74 @@ const renderTabPresets = async (tabId) => {
|
||||
if (!presetsResponse.ok) {
|
||||
throw new Error('Failed to load presets');
|
||||
}
|
||||
const allPresets = await presetsResponse.json();
|
||||
const allPresetsRaw = await presetsResponse.json();
|
||||
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
|
||||
presetsList.innerHTML = '';
|
||||
presetsList.dataset.reorderTabId = tabId;
|
||||
|
||||
// Add drag and drop handlers to the container
|
||||
presetsList.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
const dragging = presetsList.querySelector('.dragging');
|
||||
if (!dragging) return;
|
||||
// Drag-and-drop on the list (wire once — re-render would duplicate listeners otherwise)
|
||||
if (!presetsList.dataset.dragWired) {
|
||||
presetsList.dataset.dragWired = '1';
|
||||
// dragenter + dropEffect tell the browser this zone accepts a move (avoids ⊘ cursor)
|
||||
presetsList.addEventListener('dragenter', (e) => {
|
||||
if (getPresetUiMode() !== 'edit') return;
|
||||
e.preventDefault();
|
||||
});
|
||||
presetsList.addEventListener('dragover', (e) => {
|
||||
if (getPresetUiMode() !== 'edit') return;
|
||||
e.preventDefault();
|
||||
try {
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
} catch (_) {}
|
||||
const dragging = presetsList.querySelector('.dragging');
|
||||
if (!dragging) return;
|
||||
|
||||
const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
|
||||
if (dropTarget && dropTarget !== dragging) {
|
||||
// Insert before drop target so the dragged item takes that cell's position
|
||||
presetsList.insertBefore(dragging, dropTarget);
|
||||
}
|
||||
});
|
||||
const dropTarget = getDropTarget(presetsList, e.clientX, e.clientY);
|
||||
// Keep dragover side-effect free; commit placement only on drop.
|
||||
if (!dropTarget || dropTarget === dragging) {
|
||||
delete presetsList.dataset.dropTargetId;
|
||||
return;
|
||||
}
|
||||
presetsList.dataset.dropTargetId = dropTarget.dataset.presetId || '';
|
||||
});
|
||||
|
||||
presetsList.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
const dragging = presetsList.querySelector('.dragging');
|
||||
if (!dragging) return;
|
||||
presetsList.addEventListener('drop', async (e) => {
|
||||
if (getPresetUiMode() !== 'edit') return;
|
||||
e.preventDefault();
|
||||
const dragging = presetsList.querySelector('.dragging');
|
||||
if (!dragging) return;
|
||||
const targetId = presetsList.dataset.dropTargetId;
|
||||
if (targetId) {
|
||||
const dropTarget = presetsList.querySelector(`.draggable-preset[data-preset-id="${targetId}"]:not(.dragging)`);
|
||||
if (dropTarget) {
|
||||
insertDraggingOntoTarget(presetsList, dragging, dropTarget);
|
||||
}
|
||||
}
|
||||
delete presetsList.dataset.dropTargetId;
|
||||
|
||||
// Get new grid layout from DOM
|
||||
const presetElements = [...presetsList.querySelectorAll('.draggable-preset')];
|
||||
const presetIds = presetElements.map(el => el.dataset.presetId);
|
||||
const saveId = presetsList.dataset.reorderTabId;
|
||||
const presetElements = [...presetsList.querySelectorAll('.draggable-preset')];
|
||||
const presetIds = presetElements.map((el) => el.dataset.presetId);
|
||||
|
||||
// Convert to 2D grid (3 columns)
|
||||
const newGrid = arrayToGrid(presetIds, 3);
|
||||
const newGrid = arrayToGrid(presetIds, 3);
|
||||
|
||||
// Save new grid
|
||||
try {
|
||||
await savePresetGrid(tabId, newGrid);
|
||||
// Re-render to ensure consistency
|
||||
await renderTabPresets(tabId);
|
||||
} catch (error) {
|
||||
console.error('Failed to save preset grid:', error);
|
||||
alert('Failed to save preset order. Please try again.');
|
||||
// Re-render to restore original order
|
||||
await renderTabPresets(tabId);
|
||||
}
|
||||
});
|
||||
try {
|
||||
if (!saveId) {
|
||||
console.warn('No tab id for preset reorder save');
|
||||
return;
|
||||
}
|
||||
await savePresetGrid(saveId, newGrid);
|
||||
await renderTabPresets(saveId);
|
||||
} catch (error) {
|
||||
console.error('Failed to save preset grid:', error);
|
||||
alert('Failed to save preset order. Please try again.');
|
||||
const fallbackId = presetsList.dataset.reorderTabId;
|
||||
if (fallbackId) await renderTabPresets(fallbackId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get the currently selected preset for this tab
|
||||
const selectedPresetId = selectedPresets[tabId];
|
||||
@@ -1543,7 +1686,11 @@ const renderTabPresets = async (tabId) => {
|
||||
const preset = allPresets[presetId];
|
||||
if (preset) {
|
||||
const isSelected = presetId === selectedPresetId;
|
||||
const wrapper = createPresetButton(presetId, preset, tabId, isSelected);
|
||||
const displayPreset = {
|
||||
...preset,
|
||||
colors: resolveColorsWithPaletteRefs(preset.colors, preset.palette_refs, paletteColors),
|
||||
};
|
||||
const wrapper = createPresetButton(presetId, displayPreset, tabId, isSelected);
|
||||
presetsList.appendChild(wrapper);
|
||||
}
|
||||
});
|
||||
@@ -1555,15 +1702,22 @@ const renderTabPresets = async (tabId) => {
|
||||
};
|
||||
|
||||
const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
const uiMode = getPresetUiMode();
|
||||
|
||||
const row = document.createElement('div');
|
||||
const canDrag = uiMode === 'edit';
|
||||
row.className = `preset-tile-row preset-tile-row--${uiMode}${canDrag ? ' draggable-preset' : ''}`;
|
||||
row.draggable = canDrag;
|
||||
row.dataset.presetId = presetId;
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'pattern-button draggable-preset';
|
||||
button.draggable = true;
|
||||
button.dataset.presetId = presetId;
|
||||
button.type = 'button';
|
||||
button.className = 'pattern-button preset-tile-main';
|
||||
if (isSelected) {
|
||||
button.classList.add('active');
|
||||
}
|
||||
|
||||
const colors = Array.isArray(preset.colors) ? preset.colors.filter(c => c) : [];
|
||||
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']
|
||||
@@ -1584,38 +1738,76 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
presetNameLabel.className = 'pattern-button-label';
|
||||
button.appendChild(presetNameLabel);
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
button.addEventListener('click', () => {
|
||||
if (isDraggingPreset) return;
|
||||
const presetsList = document.getElementById('presets-list-tab');
|
||||
if (presetsList) {
|
||||
presetsList.querySelectorAll('.pattern-button').forEach(btn => btn.classList.remove('active'));
|
||||
const presetsListEl = document.getElementById('presets-list-tab');
|
||||
if (presetsListEl) {
|
||||
presetsListEl.querySelectorAll('.pattern-button').forEach((btn) => btn.classList.remove('active'));
|
||||
}
|
||||
button.classList.add('active');
|
||||
selectedPresets[tabId] = presetId;
|
||||
const section = button.closest('.presets-section');
|
||||
const section = row.closest('.presets-section');
|
||||
sendSelectForCurrentTabDevices(presetId, section);
|
||||
});
|
||||
|
||||
button.addEventListener('contextmenu', async (e) => {
|
||||
e.preventDefault();
|
||||
if (isDraggingPreset) return;
|
||||
await editPresetFromTab(presetId, tabId, preset);
|
||||
});
|
||||
if (canDrag) {
|
||||
row.addEventListener('dragstart', (e) => {
|
||||
isDraggingPreset = true;
|
||||
row.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', presetId);
|
||||
});
|
||||
|
||||
button.addEventListener('dragstart', (e) => {
|
||||
isDraggingPreset = true;
|
||||
button.classList.add('dragging');
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', presetId);
|
||||
});
|
||||
row.addEventListener('dragend', () => {
|
||||
row.classList.remove('dragging');
|
||||
const presetsListEl = document.getElementById('presets-list-tab');
|
||||
if (presetsListEl) {
|
||||
delete presetsListEl.dataset.dropTargetId;
|
||||
}
|
||||
document.querySelectorAll('.draggable-preset').forEach((el) => el.classList.remove('drag-over'));
|
||||
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);
|
||||
});
|
||||
row.appendChild(button);
|
||||
|
||||
return button;
|
||||
if (uiMode === 'edit') {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'preset-tile-actions';
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.type = 'button';
|
||||
editBtn.className = 'btn btn-secondary btn-small';
|
||||
editBtn.textContent = 'Edit';
|
||||
editBtn.title = 'Edit preset';
|
||||
editBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isDraggingPreset) return;
|
||||
editPresetFromTab(presetId, tabId, preset);
|
||||
});
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'btn btn-danger btn-small';
|
||||
removeBtn.textContent = 'Remove';
|
||||
removeBtn.title = 'Remove from this tab';
|
||||
removeBtn.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isDraggingPreset) return;
|
||||
if (!window.confirm('Remove this preset from this tab?')) return;
|
||||
await removePresetFromTab(tabId, presetId);
|
||||
});
|
||||
|
||||
actions.appendChild(editBtn);
|
||||
actions.appendChild(removeBtn);
|
||||
row.appendChild(actions);
|
||||
}
|
||||
|
||||
return row;
|
||||
};
|
||||
|
||||
const editPresetFromTab = async (presetId, tabId, existingPreset) => {
|
||||
@@ -1731,3 +1923,20 @@ document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateUiModeToggleButtons();
|
||||
document.querySelectorAll('.ui-mode-toggle').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const next = getPresetUiMode() === 'edit' ? 'run' : 'edit';
|
||||
setPresetUiMode(next);
|
||||
updateUiModeToggleButtons();
|
||||
const mainMenu = document.getElementById('main-menu-dropdown');
|
||||
if (mainMenu) mainMenu.classList.remove('open');
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
if (leftPanel) {
|
||||
renderTabPresets(leftPanel.dataset.tabId);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,6 +77,11 @@ header h1 {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
/* Header/menu actions that should only appear in Edit mode */
|
||||
body.preset-ui-run .edit-mode-only {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.45rem 0.9rem;
|
||||
border: none;
|
||||
@@ -596,6 +601,52 @@ header h1 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Preset tile: main button + optional edit/remove (Edit mode) */
|
||||
.preset-tile-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preset-tile-row--run .preset-tile-actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.preset-tile-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 5rem;
|
||||
}
|
||||
|
||||
.preset-tile-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
padding: 0.15rem 0 0.15rem 0.25rem;
|
||||
}
|
||||
|
||||
.preset-tile-actions .btn {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
padding: 0.15rem 0.35rem;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.15;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ui-mode-toggle--edit {
|
||||
background-color: #4a3f8f;
|
||||
border: 1px solid #7b6fd6;
|
||||
}
|
||||
|
||||
.ui-mode-toggle--edit:hover {
|
||||
background-color: #5a4f9f;
|
||||
}
|
||||
|
||||
/* Preset select buttons inside the tab grid */
|
||||
#presets-list-tab .pattern-button {
|
||||
display: flex;
|
||||
|
||||
+24
-17
@@ -15,24 +15,26 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-secondary" id="tabs-btn">Tabs</button>
|
||||
<button class="btn btn-secondary" id="color-palette-btn">Color Palette</button>
|
||||
<button class="btn btn-secondary" id="presets-btn">Presets</button>
|
||||
<button class="btn btn-secondary edit-mode-only" id="tabs-btn">Tabs</button>
|
||||
<button class="btn btn-secondary edit-mode-only" id="color-palette-btn">Color Palette</button>
|
||||
<button class="btn btn-secondary edit-mode-only" id="presets-btn">Presets</button>
|
||||
<button class="btn btn-secondary" id="send-profile-presets-btn">Send Presets</button>
|
||||
<button class="btn btn-secondary" id="patterns-btn">Patterns</button>
|
||||
<button class="btn btn-secondary" id="profiles-btn">Profiles</button>
|
||||
<button class="btn btn-secondary edit-mode-only" id="patterns-btn">Patterns</button>
|
||||
<button class="btn btn-secondary edit-mode-only" id="profiles-btn">Profiles</button>
|
||||
<button class="btn btn-secondary" id="settings-btn">Settings</button>
|
||||
<button class="btn btn-secondary" id="help-btn">Help</button>
|
||||
<button type="button" class="btn btn-secondary ui-mode-toggle" id="ui-mode-toggle" aria-pressed="false" title="Switch preset strip mode — label is the mode you will switch to">Edit mode</button>
|
||||
</div>
|
||||
<div class="header-menu-mobile">
|
||||
<button class="btn btn-secondary" id="main-menu-btn">Menu</button>
|
||||
<div id="main-menu-dropdown" class="main-menu-dropdown">
|
||||
<button type="button" data-target="tabs-btn">Tabs</button>
|
||||
<button type="button" data-target="color-palette-btn">Color Palette</button>
|
||||
<button type="button" data-target="presets-btn">Presets</button>
|
||||
<button type="button" class="ui-mode-toggle" id="ui-mode-toggle-mobile" aria-pressed="false">Edit mode</button>
|
||||
<button type="button" class="edit-mode-only" data-target="tabs-btn">Tabs</button>
|
||||
<button type="button" class="edit-mode-only" data-target="color-palette-btn">Color Palette</button>
|
||||
<button type="button" class="edit-mode-only" data-target="presets-btn">Presets</button>
|
||||
<button type="button" data-target="send-profile-presets-btn">Send Presets</button>
|
||||
<button type="button" data-target="patterns-btn">Patterns</button>
|
||||
<button type="button" data-target="profiles-btn">Profiles</button>
|
||||
<button type="button" class="edit-mode-only" data-target="patterns-btn">Patterns</button>
|
||||
<button type="button" class="edit-mode-only" data-target="profiles-btn">Profiles</button>
|
||||
<button type="button" data-target="settings-btn">Settings</button>
|
||||
<button type="button" data-target="help-btn">Help</button>
|
||||
</div>
|
||||
@@ -92,6 +94,12 @@
|
||||
<input type="text" id="new-profile-name" placeholder="Profile name">
|
||||
<button class="btn btn-primary" id="create-profile-btn">Create</button>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 0.5rem; margin-bottom: 0;">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0;">
|
||||
<input type="checkbox" id="new-profile-seed-dj">
|
||||
DJ tab
|
||||
</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>
|
||||
@@ -126,9 +134,8 @@
|
||||
<label>Colors</label>
|
||||
<div id="preset-colors-container" class="preset-colors-container"></div>
|
||||
<div class="profiles-actions">
|
||||
<input type="color" id="preset-new-color" value="#ffffff">
|
||||
<button class="btn btn-secondary btn-small" id="preset-add-color-btn">Add Color</button>
|
||||
<button class="btn btn-secondary btn-small" id="preset-add-from-palette-btn">Add from Palette</button>
|
||||
<input type="color" id="preset-new-color" value="#ffffff" title="Choose color (auto-adds)">
|
||||
<button class="btn btn-secondary btn-small" id="preset-add-from-palette-btn">From Palette</button>
|
||||
</div>
|
||||
<div class="profiles-actions">
|
||||
<div class="preset-editor-field">
|
||||
@@ -204,7 +211,6 @@
|
||||
<div id="palette-container" class="profiles-list"></div>
|
||||
<div class="profiles-actions">
|
||||
<input type="color" id="palette-new-color" value="#ffffff">
|
||||
<button class="btn btn-primary" id="palette-add-color-btn">Add Color</button>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="color-palette-close-btn">Close</button>
|
||||
@@ -228,9 +234,10 @@
|
||||
<h3>Presets in a tab</h3>
|
||||
<ul>
|
||||
<li><strong>Select preset</strong>: left-click a preset tile to select it and send a <code>select</code> message to all devices in the tab.</li>
|
||||
<li><strong>Edit preset</strong>: right-click a preset tile and choose <strong>Edit preset…</strong>.</li>
|
||||
<li><strong>Remove from tab</strong>: right-click a preset tile and choose <strong>Remove from this tab</strong> (the preset itself is not deleted, only its link from this tab).</li>
|
||||
<li><strong>Reorder presets</strong>: drag preset tiles to change their order; the new layout is saved automatically.</li>
|
||||
<li><strong>Run vs Edit mode</strong>: use the mode button in the menu (it shows the mode you will <em>switch to</em>). In <strong>Edit mode</strong>, each preset tile shows <strong>Edit</strong> and <strong>Remove</strong> on the right.</li>
|
||||
<li><strong>Edit preset</strong>: switch to <strong>Edit mode</strong> (menu button) and use <strong>Edit</strong> on each tile.</li>
|
||||
<li><strong>Remove from tab</strong>: in <strong>Edit mode</strong>, use <strong>Remove</strong> on the tile (the preset itself is not deleted, only its link from this tab).</li>
|
||||
<li><strong>Reorder presets</strong>: in <strong>Edit mode</strong> only, drag tiles to change order; the layout saves automatically.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Presets, profiles & colors</h3>
|
||||
|
||||
@@ -533,7 +533,7 @@ def test_mobile_tab_presets_two_columns():
|
||||
container = bt.wait_for_element(By.ID, 'presets-list-tab', timeout=10)
|
||||
assert container is not None, "presets-list-tab not found"
|
||||
|
||||
tiles = bt.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .draggable-preset')
|
||||
tiles = bt.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .preset-tile-row')
|
||||
# Need at least 2 presets to make this meaningful
|
||||
assert len(tiles) >= 2, "Fewer than 2 presets found for tab"
|
||||
|
||||
@@ -882,7 +882,7 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Test 5: Find presets in tab and test drag and drop
|
||||
# Test 5: Find presets in tab and test drag and drop (Edit mode only)
|
||||
total += 1
|
||||
try:
|
||||
# Wait for presets to load in the tab
|
||||
@@ -890,6 +890,12 @@ def test_preset_drag_and_drop(browser: BrowserTest) -> bool:
|
||||
if presets_list_tab:
|
||||
time.sleep(1) # Wait for presets to render
|
||||
|
||||
# Reordering is only available in Edit mode (tiles get .draggable-preset)
|
||||
mode_toggle = browser.wait_for_element(By.CSS_SELECTOR, '.ui-mode-toggle', timeout=5)
|
||||
if mode_toggle and mode_toggle.get_attribute('aria-pressed') == 'false':
|
||||
mode_toggle.click()
|
||||
time.sleep(0.5)
|
||||
|
||||
# Find draggable preset elements - wait a bit more for rendering
|
||||
time.sleep(1)
|
||||
draggable_presets = browser.driver.find_elements(By.CSS_SELECTOR, '#presets-list-tab .draggable-preset')
|
||||
|
||||
Reference in New Issue
Block a user