feat: color picker

This commit is contained in:
2025-09-04 20:33:33 +02:00
parent 3e1c6eaef0
commit eb50048016
2 changed files with 116 additions and 9 deletions

View File

@@ -5,6 +5,28 @@ class NodeDetailsComponent extends Component {
this.suppressLoadingUI = false;
}
// Helper functions for color conversion
rgbIntToHex(rgbInt) {
if (!rgbInt && rgbInt !== 0) return '#000000';
const num = parseInt(rgbInt);
if (isNaN(num)) return '#000000';
const r = (num >> 16) & 255;
const g = (num >> 8) & 255;
const b = num & 255;
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
hexToRgbInt(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return 0;
const r = parseInt(result[1], 16);
const g = parseInt(result[2], 16);
const b = parseInt(result[3], 16);
return (r << 16) + (g << 8) + b;
}
setupViewModelListeners() {
this.subscribeToProperty('nodeStatus', this.handleNodeStatusUpdate.bind(this));
this.subscribeToProperty('tasks', this.handleTasksUpdate.bind(this));
@@ -421,7 +443,31 @@ class NodeDetailsComponent extends Component {
<span class="param-name">${p.name}${p.required ? ' *' : ''}</span>
${ (Array.isArray(p.values) && p.values.length > 1)
? `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`
: `<input id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input" type="text" placeholder="${p.location || 'body'}${p.type || 'string'}" value="${p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : ''}">`
: (p.type === 'color')
? `<div class="color-input-container">
<input id="${formId}-field-${pidx}"
data-param-name="${p.name}"
data-param-location="${p.location || 'body'}"
data-param-type="${p.type || 'string'}"
data-param-required="${p.required ? '1' : '0'}"
class="param-input color-picker"
type="color"
value="${this.rgbIntToHex(p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : 0)}">
<input type="text"
class="color-rgb-display"
readonly
value="${p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : 0}"
style="margin-left: 5px; width: 80px; font-size: 0.8em;">
</div>`
: `<input id="${formId}-field-${pidx}"
data-param-name="${p.name}"
data-param-location="${p.location || 'body'}"
data-param-type="${p.type || 'string'}"
data-param-required="${p.required ? '1' : '0'}"
class="param-input"
type="text"
placeholder="${p.location || 'body'}${p.type || 'string'}"
value="${p.default !== undefined ? p.default : (Array.isArray(p.values) && p.values.length === 1) ? p.values[0] : ''}">`
}
</label>
`).join('')}</div>`
@@ -486,13 +532,21 @@ class NodeDetailsComponent extends Component {
if (!formEl || !resultEl) return;
const inputs = Array.from(formEl.querySelectorAll('.param-input'));
const params = inputs.map(input => ({
name: input.dataset.paramName,
location: input.dataset.paramLocation || 'body',
type: input.dataset.paramType || 'string',
required: input.dataset.paramRequired === '1',
value: input.value
}));
const params = inputs.map(input => {
let value = input.value;
// For color type, convert hex to RGB integer
if (input.dataset.paramType === 'color' && input.type === 'color') {
const rgbDisplay = input.parentElement.querySelector('.color-rgb-display');
value = rgbDisplay ? rgbDisplay.value : this.hexToRgbInt(input.value);
}
return {
name: input.dataset.paramName,
location: input.dataset.paramLocation || 'body',
type: input.dataset.paramType || 'string',
required: input.dataset.paramRequired === '1',
value: value
};
});
// Required validation
const missing = params.filter(p => p.required && (!p.value || String(p.value).trim() === ''));
@@ -528,6 +582,32 @@ class NodeDetailsComponent extends Component {
}
});
});
// Add event listeners for color pickers
const colorPickers = this.findAllElements('.color-picker');
colorPickers.forEach(colorInput => {
this.addEventListener(colorInput, 'input', (e) => {
const rgbDisplay = colorInput.parentElement.querySelector('.color-rgb-display');
if (rgbDisplay) {
const hexValue = e.target.value;
const rgbInt = this.hexToRgbInt(hexValue);
rgbDisplay.value = rgbInt;
}
});
});
// Update color picker when RGB display is manually changed (if we make it editable later)
const rgbDisplays = this.findAllElements('.color-rgb-display');
rgbDisplays.forEach(rgbInput => {
this.addEventListener(rgbInput, 'input', (e) => {
const colorPicker = rgbInput.parentElement.querySelector('.color-picker');
if (colorPicker) {
const rgbInt = parseInt(e.target.value) || 0;
const hexValue = this.rgbIntToHex(rgbInt);
colorPicker.value = hexValue;
}
});
});
}
escapeHtml(str) {

View File

@@ -3771,3 +3771,30 @@ html {
#cluster-members-container .error br {
display: none; /* tighten layout by avoiding forced line-breaks */
}
/* Color picker styles */
.color-input-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.color-picker {
width: 50px !important;
height: 35px !important;
padding: 0 !important;
border-radius: 6px !important;
cursor: pointer;
}
.color-rgb-display {
background: rgba(0, 0, 0, 0.3) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
color: rgba(255, 255, 255, 0.9) !important;
text-align: center;
font-family: 'Courier New', monospace !important;
}
.color-rgb-display:focus {
border-color: rgba(139, 92, 246, 0.5) !important;
background: rgba(139, 92, 246, 0.08) !important;
}