feat: color picker
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -3770,4 +3770,31 @@ 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user