Add drag-and-drop for presets and colors, max_colors validation, and 2D grid layout

- Add drag-and-drop to reorder presets in tabs (2D grid layout)
- Add drag-and-drop to reorder colors within presets
- Add max_colors field to pattern definitions
- Hide color section when max_colors is 0
- Validate color count against pattern max_colors limit
- Store presets in 2D grid format (3 columns)
- Remove left panel from tab content, show only presets
- Update color palette to show swatches instead of hex codes
- Improve preset editor UI with visual color swatches
This commit is contained in:
2026-01-17 00:58:50 +13:00
parent 9f37dbbff0
commit 97ffc69b12
6 changed files with 1183 additions and 174 deletions

View File

@@ -1,46 +1,54 @@
{
"on": {
"min_delay": 10,
"max_delay": 10000
"max_delay": 10000,
"max_colors": 1
},
"off": {
"min_delay": 10,
"max_delay": 10000
},
"rainbow": {
"Step Rate": "n1",
"min_delay": 10,
"max_delay": 10000
},
"transition": {
"min_delay": 10,
"max_delay": 10000
},
"chase": {
"Colour 1 Length": "n1",
"Colour 2 Length": "n2",
"Step 1": "n3",
"Step 2": "n4",
"min_delay": 10,
"max_delay": 10000
},
"pulse": {
"Attack": "n1",
"Hold": "n2",
"Decay": "n3",
"min_delay": 10,
"max_delay": 10000
},
"circle": {
"Head Rate": "n1",
"Max Length": "n2",
"Tail Rate": "n3",
"Min Length": "n4",
"min_delay": 10,
"max_delay": 10000
},
"blink": {
"min_delay": 10,
"max_delay": 10000
}
}
"off": {
"min_delay": 10,
"max_delay": 10000,
"max_colors": 0
},
"rainbow": {
"n1": "Step Rate",
"min_delay": 10,
"max_delay": 10000,
"max_colors": 0
},
"transition": {
"min_delay": 10,
"max_delay": 10000,
"max_colors": 10
},
"chase": {
"n1": "Colour 1 Length",
"n2": "Colour 2 Length",
"n3": "Step 1",
"n4": "Step 2",
"min_delay": 10,
"max_delay": 10000,
"max_colors": 2
},
"pulse": {
"n1": "Attack",
"n2": "Hold",
"n3": "Decay",
"min_delay": 10,
"max_delay": 10000,
"max_colors": 10
},
"circle": {
"n1": "Head Rate",
"n2": "Max Length",
"n3": "Tail Rate",
"n4": "Min Length",
"min_delay": 10,
"max_delay": 10000,
"max_colors": 2
},
"blink": {
"min_delay": 10,
"max_delay": 10000,
"max_colors": 10
}
}

View File

@@ -163,88 +163,17 @@ async def tab_content_fragment(request, session, id):
return send_file('templates/index.html')
tab_name = tab.get('name', 'Tab ' + str(id))
device_ids = ', '.join(tab.get('names', []))
html = (
'<div class="left-panel">'
'<div class="left-panel-header">'
'<div class="ids-display">'
'<label>IDs: </label>'
'<span id="current-ids">' + device_ids + '</span>'
'</div>'
'<button id="toggle-left-panel" class="btn btn-small left-panel-toggle" title="Collapse/expand controls">◀</button>'
'</div>'
'<div class="left-panel-body">'
'<div class="color-palette-section">'
'<h3>Color Palette</h3>'
'<div id="color-palette" class="color-palette" data-tab-id="' + str(id) + '">'
'<!-- Colors will be loaded here -->'
'</div>'
'<div class="palette-actions">'
'<input type="color" id="tab-color-input" value="#ffffff">'
'<button class="btn btn-small" id="tab-color-add-btn">Add Color</button>'
'<button class="btn btn-small" id="tab-color-add-from-palette-btn">Add from Palette</button>'
'</div>'
'</div>'
'<div class="controls-section">'
'<div class="control-group">'
'<label for="brightness-slider">Brightness:</label>'
'<input type="range" id="brightness-slider" min="0" max="255" value="127" class="slider">'
'<span id="brightness-value" class="slider-value">127</span>'
'</div>'
'<div class="control-group">'
'<label for="delay-slider">Delay:</label>'
'<input type="range" id="delay-slider" min="0" max="1000" value="0" class="slider">'
'<span id="delay-value" class="slider-value">100 ms</span>'
'</div>'
'</div>'
'<div class="n-params-section">'
'<h3>N Parameters</h3>'
'<div class="n-params-grid">'
'<div class="n-param-group">'
'<label for="n1-input" id="n1-label">n1:</label>'
'<input type="number" id="n1-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n2-input" id="n2-label">n2:</label>'
'<input type="number" id="n2-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n3-input" id="n3-label">n3:</label>'
'<input type="number" id="n3-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n4-input" id="n4-label">n4:</label>'
'<input type="number" id="n4-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n5-input" id="n5-label">n5:</label>'
'<input type="number" id="n5-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n6-input" id="n6-label">n6:</label>'
'<input type="number" id="n6-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n7-input" id="n7-label">n7:</label>'
'<input type="number" id="n7-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'<div class="n-param-group">'
'<label for="n8-input" id="n8-label">n8:</label>'
'<input type="number" id="n8-input" min="0" max="255" value="10" class="n-input">'
'</div>'
'</div>'
'</div>'
'</div>'
'</div>'
'<div class="right-panel">'
'<div class="presets-section">'
'<div class="presets-section" data-tab-id="' + str(id) + '">'
'<h3>Presets</h3>'
'<div class="profiles-actions" style="margin-bottom: 1rem;">'
'<button class="btn btn-primary" id="preset-add-btn-tab">Add Preset</button>'
'</div>'
'<div id="presets-list-tab" class="presets-list">'
'<!-- Presets will be loaded here -->'
'</div>'
'</div>'
'</div>'
)
return html, 200, {'Content-Type': 'text/html'}

View File

@@ -28,27 +28,35 @@ document.addEventListener('DOMContentLoaded', () => {
const row = document.createElement('div');
row.className = 'profiles-row';
row.dataset.color = color;
row.style.cssText = 'display: flex; align-items: center; gap: 1rem;';
// Ensure no text content
row.textContent = '';
const swatch = document.createElement('div');
swatch.style.width = '28px';
swatch.style.height = '28px';
swatch.style.borderRadius = '4px';
swatch.style.backgroundColor = color;
swatch.style.border = '1px solid #4a4a4a';
const label = document.createElement('span');
label.textContent = color;
swatch.style.cssText = `
width: 64px;
height: 64px;
border-radius: 8px;
background-color: ${color};
border: 2px solid #4a4a4a;
cursor: pointer;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
`;
swatch.title = color; // Show hex code on hover only
swatch.setAttribute('aria-label', `Color ${color}`);
const removeButton = document.createElement('button');
removeButton.className = 'btn btn-danger btn-small';
removeButton.textContent = 'Remove';
removeButton.addEventListener('click', async () => {
removeButton.style.fontSize = '0.8rem'; // Restore font size for button
removeButton.addEventListener('click', async (e) => {
e.stopPropagation();
const updated = currentPalette.filter((_, i) => i !== index);
await savePalette(updated);
});
row.appendChild(swatch);
row.appendChild(label);
row.appendChild(removeButton);
paletteContainer.appendChild(row);
});

File diff suppressed because it is too large Load Diff

View File

@@ -364,7 +364,7 @@ header h1 {
.presets-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}

View File

@@ -129,10 +129,12 @@
<option value="">Pattern</option>
</select>
</div>
<label>Colors (comma-separated hex)</label>
<div class="profiles-actions">
<input type="text" id="preset-colors-input" placeholder="#FF0000,#00FF00,#0000FF">
<button class="btn btn-secondary" id="preset-add-from-palette-btn">Add from Palette</button>
<label>Colors</label>
<div id="preset-colors-container" class="preset-colors-container"></div>
<div class="profiles-actions" style="margin-top: 0.5rem;">
<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>
</div>
<div class="profiles-actions">
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
@@ -268,6 +270,34 @@
background-color: #3a3a3a;
border-radius: 4px;
}
/* Hide any text content in palette rows - only show color swatches */
#palette-container .profiles-row {
font-size: 0; /* Hide any text nodes */
}
#palette-container .profiles-row > * {
font-size: 1rem; /* Restore font size for buttons */
}
#palette-container .profiles-row > span:not(.btn),
#palette-container .profiles-row > label,
#palette-container .profiles-row::before,
#palette-container .profiles-row::after {
display: none !important;
content: none !important;
}
/* Preset colors container */
#preset-colors-container {
min-height: 80px;
padding: 0.5rem;
background-color: #2a2a2a;
border-radius: 4px;
margin-bottom: 0.5rem;
}
#preset-colors-container .muted-text {
color: #888;
font-size: 0.9rem;
padding: 1rem;
text-align: center;
}
.muted-text {
text-align: center;
color: #888;
@@ -285,6 +315,38 @@
border-radius: 4px;
margin-top: 0.5rem;
}
/* Drag and drop styles for presets */
.draggable-preset {
cursor: move;
transition: opacity 0.2s, transform 0.2s;
}
.draggable-preset.dragging {
opacity: 0.5;
transform: scale(0.95);
}
.draggable-preset:hover {
opacity: 0.8;
}
/* Drag and drop styles for color swatches */
.draggable-color-swatch {
transition: opacity 0.2s, transform 0.2s;
}
.draggable-color-swatch.dragging-color {
opacity: 0.5;
transform: scale(0.9);
}
.draggable-color-swatch.drag-over-color {
transform: scale(1.1);
}
.color-swatches-container {
min-height: 80px;
}
/* Ensure presets list uses grid layout */
#presets-list-tab {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
</style>
<script src="/static/color_palette.js"></script>
<script src="/static/profiles.js"></script>