diff --git a/server/app/static/frame_config.js b/server/app/static/frame_config.js index bf6fc11..bbaf73f 100644 --- a/server/app/static/frame_config.js +++ b/server/app/static/frame_config.js @@ -67,10 +67,56 @@ async function loadControl() { document.getElementById('take-control').addEventListener('click', takeControl); // ---- Advanced configuration: color palette ---- +// +// Hex field and R/G/B number fields are kept in sync live, both +// directions -- editing either updates the other plus the preview +// swatch. Hex stays the field actually read at save time (it's what +// the server already validates as #rrggbb); the R/G/B fields are purely +// an alternate, more precise way to arrive at the same value than +// eyeballing a color-picker swatch. -function paletteInputs() { - return Array.from(document.querySelectorAll('[id^="palette_"]')) - .sort((a, b) => Number(a.id.split('_')[1]) - Number(b.id.split('_')[1])); +function paletteHexInputs() { + return Array.from(document.querySelectorAll('.palette-hex')) + .sort((a, b) => Number(a.dataset.index) - Number(b.dataset.index)); +} + +function hexFromRgb(r, g, b) { + const clamp = (v) => Math.max(0, Math.min(255, Math.round(Number(v) || 0))); + return '#' + [r, g, b].map((v) => clamp(v).toString(16).padStart(2, '0')).join(''); +} + +function rgbFromHex(hex) { + const m = /^#?([0-9a-f]{6})$/i.exec((hex || '').trim()); + if (!m) return null; + const n = parseInt(m[1], 16); + return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; +} + +function paletteFieldsFor(index) { + const at = (cls) => document.querySelector(`.${cls}[data-index="${index}"]`); + return { hex: at('palette-hex'), r: at('palette-r'), g: at('palette-g'), b: at('palette-b'), swatch: at('palette-swatch-preview') }; +} + +function syncPaletteFromHex(index) { + const f = paletteFieldsFor(index); + const rgb = rgbFromHex(f.hex.value); + if (!rgb) return; + [f.r.value, f.g.value, f.b.value] = rgb; + f.swatch.style.background = f.hex.value; +} + +function syncPaletteFromRgb(index) { + const f = paletteFieldsFor(index); + const hex = hexFromRgb(f.r.value, f.g.value, f.b.value); + f.hex.value = hex; + f.swatch.style.background = hex; +} + +const palettePickerCount = paletteHexInputs().length; +for (let i = 0; i < palettePickerCount; i++) { + const f = paletteFieldsFor(i); + f.hex.addEventListener('input', () => syncPaletteFromHex(i)); + [f.r, f.g, f.b].forEach((el) => el.addEventListener('input', () => syncPaletteFromRgb(i))); } async function savePalette(body) { @@ -89,15 +135,18 @@ async function savePalette(body) { document.getElementById('palette-save').addEventListener('click', () => { const body = new URLSearchParams(); - for (const input of paletteInputs()) { + for (const input of paletteHexInputs()) { body.append('palette', input.value); } savePalette(body); }); document.getElementById('palette-reset').addEventListener('click', () => { - const inputs = paletteInputs(); - window.DEFAULT_PALETTE_HEX.forEach((hex, i) => { inputs[i].value = hex; }); + const inputs = paletteHexInputs(); + window.DEFAULT_PALETTE_HEX.forEach((hex, i) => { + inputs[i].value = hex; + syncPaletteFromHex(i); + }); savePalette(new URLSearchParams({ palette_reset: 'true' })); }); diff --git a/server/app/static/theme.css b/server/app/static/theme.css index 716b160..e337bae 100644 --- a/server/app/static/theme.css +++ b/server/app/static/theme.css @@ -168,28 +168,33 @@ summary.card-title { cursor: pointer; margin-bottom: 0; } details.card[open] summary.card-title { margin-bottom: 14px; } details.card .sub { margin-top: 8px; } -.palette-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); - gap: 12px; - margin-top: 14px; +.palette-table-wrap { overflow-x: auto; margin-top: 14px; } +.palette-table { width: 100%; border-collapse: collapse; font-size: 13px; } +.palette-table th { + text-align: left; + color: var(--text-muted); + font-weight: 600; + font-size: 11.5px; + text-transform: uppercase; + letter-spacing: 0.02em; + padding: 0 8px 8px 0; } -.palette-swatch { - display: flex; - align-items: center; - gap: 8px; - margin-top: 0; - font-size: 13px; - font-weight: 500; - color: var(--text); +.palette-table td { padding: 4px 8px 4px 0; vertical-align: middle; } +.palette-table input { margin-top: 0; } +.palette-swatch-preview { + display: block; + width: 22px; + height: 22px; + border-radius: 6px; + border: 1px solid var(--border); } -.palette-swatch input[type="color"] { - width: 34px; - height: 34px; - padding: 2px; - margin-top: 0; - border-radius: 8px; - cursor: pointer; +.palette-table input.palette-hex { + width: 92px; + font-family: ui-monospace, "SF Mono", Consolas, monospace; + text-transform: lowercase; +} +.palette-table input.palette-rgb { + width: 58px; } .card { diff --git a/server/app/templates/frame_config.html b/server/app/templates/frame_config.html index 018bea0..62a5505 100644 --- a/server/app/templates/frame_config.html +++ b/server/app/templates/frame_config.html @@ -120,14 +120,30 @@ aren't published. Tune them by comparing a rendered photo against the physical panel; different panel units can vary enough to be worth calibrating per frame.

-
- {% set current_hex = palette_to_hex(frame.palette_rgb or default_palette_rgb) %} - {% for label in palette_labels %} - - {% endfor %} +
+ + + + {% set current_palette = frame.palette_rgb or default_palette_rgb %} + {% set current_hex = palette_to_hex(current_palette) %} + {% for label in palette_labels %} + + + + + + + + + {% endfor %} + +
ColorHexRGB
{{ label }}