From eb500480161a6dd39d533d51a4c33f526738b5d9 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Thu, 4 Sep 2025 20:33:33 +0200 Subject: [PATCH] feat: color picker --- .../components/NodeDetailsComponent.js | 96 +++++++++++++++++-- public/styles/main.css | 29 +++++- 2 files changed, 116 insertions(+), 9 deletions(-) diff --git a/public/scripts/components/NodeDetailsComponent.js b/public/scripts/components/NodeDetailsComponent.js index 196518d..7d88f02 100644 --- a/public/scripts/components/NodeDetailsComponent.js +++ b/public/scripts/components/NodeDetailsComponent.js @@ -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 { ${p.name}${p.required ? ' *' : ''} ${ (Array.isArray(p.values) && p.values.length > 1) ? `` - : `` + : (p.type === 'color') + ? `
+ + +
` + : `` } `).join('')}` @@ -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) { diff --git a/public/styles/main.css b/public/styles/main.css index 8e1d7b8..3ec0bf1 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -3770,4 +3770,31 @@ html { #cluster-members-container .error br { display: none; /* tighten layout by avoiding forced line-breaks */ -} \ No newline at end of file +} +/* 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; +}