diff --git a/apps/overlay/src/index.html b/apps/overlay/src/index.html index e68e88c..9835117 100644 --- a/apps/overlay/src/index.html +++ b/apps/overlay/src/index.html @@ -148,6 +148,30 @@ .setting-row input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; } .setting-row input[type="color"]::-webkit-color-swatch { border: none; border-radius: 2px; } + /* Text entry beside each color swatch (#91): type a hex (#RGB / #RRGGBB) or + rgb(r,g,b) value instead of using the native picker. The swatch and text + field stay in sync. */ + .color-combo { display: flex; align-items: center; gap: 6px; } + .color-text { + width: 78px; + background: rgba(255,255,255,0.08); + color: #eee; + border: 1px solid rgba(255,255,255,0.12); + border-radius: 4px; + padding: 3px 5px; + font: 11px ui-monospace, Consolas, 'Courier New', monospace; + text-transform: lowercase; + } + .color-text:focus { outline: none; border-color: #3388ff; } + .color-text.invalid { border-color: #ff5555; color: #ff9999; } + /* Grouped color rows (Roll / Axis): label on its own line, then a full-width + row of swatch-over-text cells so all three fit the narrow panel. */ + .setting-row.color-row-stacked { flex-direction: column; align-items: stretch; gap: 6px; } + .color-group { display: flex; gap: 8px; } + .color-group .color-combo { flex: 1; min-width: 0; flex-direction: column; gap: 3px; align-items: stretch; } + .color-group .color-combo input[type="color"] { width: 100%; } + .color-group .color-text { width: 100%; text-align: center; } + .setting-row input[type="checkbox"] { width: 14px; height: 14px; accent-color: #3388ff; @@ -1020,9 +1044,9 @@

Window Display

-
+
- + @@ -1036,9 +1060,9 @@

Window Display

-
+
- + diff --git a/apps/overlay/src/js/app.js b/apps/overlay/src/js/app.js index e3dd28d..593cfe2 100644 --- a/apps/overlay/src/js/app.js +++ b/apps/overlay/src/js/app.js @@ -2236,6 +2236,68 @@ function applyGreenScreen() { greenScreenToggle.addEventListener('change', applyGreenScreen); greenScreenColorInput.addEventListener('input', applyGreenScreen); +// ── Type-a-color (#91) ──────────────────────────────────────────────────── +// Every color setting is a native (swatch-only). Let the +// user also type a value: accept hex (#RGB / #RRGGBB) or rgb(r,g,b) / rgba(...), +// normalize to #rrggbb, and on a valid parse write it back to the swatch and +// dispatch the swatch's 'input' event — so every existing per-picker apply + +// persist handler runs unchanged. Picker→text sync keeps the field current when +// the native swatch is used. Runs last so each swatch's value is already +// initialized from localStorage before we mirror it into the text field. +function normalizeColor(value) { + if (value == null) return null; + const v = String(value).trim().toLowerCase(); + let m = v.match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/); + if (m) { + let h = m[1]; + if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]; + return '#' + h; + } + m = v.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+\s*)?\)$/); + if (m) { + const c = [m[1], m[2], m[3]].map(Number); + if (c.every((n) => n <= 255)) return '#' + c.map((n) => n.toString(16).padStart(2, '0')).join(''); + } + return null; +} + +function attachColorText(colorInput) { + const combo = document.createElement('span'); + combo.className = 'color-combo'; + const text = document.createElement('input'); + text.type = 'text'; + text.className = 'color-text'; + text.spellcheck = false; + text.autocomplete = 'off'; + text.setAttribute('aria-label', (colorInput.id || 'color') + ' hex or rgb value'); + text.value = colorInput.value; + // Wrap the swatch: text field first, swatch second (CSS stacks them for the + // grouped rows). Moving the swatch in the DOM keeps its event listeners. + colorInput.parentNode.insertBefore(combo, colorInput); + combo.appendChild(text); + combo.appendChild(colorInput); + + const commit = () => { + const hex = normalizeColor(text.value); + if (!hex) { text.classList.add('invalid'); return; } + text.classList.remove('invalid'); + text.value = hex; + if (colorInput.value.toLowerCase() !== hex) { + colorInput.value = hex; + colorInput.dispatchEvent(new Event('input', { bubbles: true })); + } + }; + text.addEventListener('change', commit); + text.addEventListener('keydown', (e) => { if (e.key === 'Enter') { commit(); text.blur(); } }); + // Native swatch moved → reflect it in the text field. + colorInput.addEventListener('input', () => { + text.value = colorInput.value; + text.classList.remove('invalid'); + }); +} + +document.querySelectorAll('#settings-panel input[type="color"]').forEach(attachColorText); + // Camera presets — one selected at a time, used as calibration view. // Defaults to Top for every controller; the last-selected preset is persisted // (issue #70) so reopening the overlay restores the user's preferred view