From a71b14d75502ba1f65b5ad86c25b0968cb70680b Mon Sep 17 00:00:00 2001 From: Waters Church Date: Thu, 10 Sep 2026 16:31:46 -0400 Subject: [PATCH] Shell: `color` field type (swatch picker) in tile settings and admin config Renders with the label beside it; the stored value is always a #rrggbb string (3-digit hex widened, anything else falls back to the schema default). Same styling in the gear popover and the admin form. Co-Authored-By: Claude Fable 5.1 --- docs/MODULE-GUIDE.md | 4 +++- public/admin/admin.js | 11 +++++++++++ public/shell.js | 18 ++++++++++++++++++ public/style.css | 21 +++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/docs/MODULE-GUIDE.md b/docs/MODULE-GUIDE.md index d6cd507..ca7bdd0 100644 --- a/docs/MODULE-GUIDE.md +++ b/docs/MODULE-GUIDE.md @@ -64,7 +64,7 @@ shell code changes, ever. Both schemas map field names to specs: ``` -{ "type": "string" | "number" | "boolean" | "switch" | "select" | "password" | "endpoint", +{ "type": "string" | "number" | "boolean" | "switch" | "select" | "color" | "password" | "endpoint", "label": "Shown next to the field", "default": , "group": "Section heading", // optional; see below @@ -78,6 +78,8 @@ Both schemas map field names to specs: `boolean` renders as a checkbox; `switch` is the same true/false value shown as a toggle (use it for a feature on/off that other fields depend on). +`color` renders a swatch picker and stores a `#rrggbb` string (give it a hex +default; the tile applies it as a CSS custom property — see `pco-plan`). `group` gathers consecutive fields under a labeled subsection in the admin page, so a module with several distinct concerns (e.g. a ProPresenter diff --git a/public/admin/admin.js b/public/admin/admin.js index 891c28b..151c4dd 100644 --- a/public/admin/admin.js +++ b/public/admin/admin.js @@ -209,6 +209,17 @@ function fieldFor(key, spec, value, passwordSet, moduleId) { field.append(span, select); read = () => select.value; selectEl = select; + } else if (type === 'color') { + // A swatch picker; the value is always a #rrggbb string. + field.classList.add('color-field'); + const span = document.createElement('span'); + span.textContent = label; + const input = document.createElement('input'); + input.type = 'color'; + const hex = (v, fb) => (/^#[0-9a-fA-F]{6}$/.test(String(v ?? '').trim()) ? String(v).trim().toLowerCase() : fb); + input.value = hex(value, hex(spec?.default, '#2ee59a')); + field.append(span, input); + read = () => input.value; } else { const span = document.createElement('span'); span.textContent = label; diff --git a/public/shell.js b/public/shell.js index 0ae98f3..5a8fc98 100644 --- a/public/shell.js +++ b/public/shell.js @@ -919,6 +919,14 @@ function unmountModule(tile) { /* ── per-tile settings popover ──────────────────────────────────────── */ +/** `` only accepts #rrggbb — anything else falls back. */ +function colorHex(value, fallback) { + const s = String(value ?? '').trim(); + if (/^#[0-9a-fA-F]{6}$/.test(s)) return s.toLowerCase(); + if (/^#[0-9a-fA-F]{3}$/.test(s)) return ('#' + s.slice(1).split('').map((c) => c + c).join('')).toLowerCase(); + return fallback; +} + function closeSettingsPopovers() { document.querySelectorAll('.tile-settings').forEach((p) => p.remove()); } @@ -1004,6 +1012,16 @@ function toggleSettingsPopover(tile, el) { select.value = String(current ?? ''); field.append(span, select); inputs.set(key, () => select.value); + } else if (type === 'color') { + // A swatch picker; the value is always a #rrggbb string. + field.classList.add('color-field'); + const span = document.createElement('span'); + span.textContent = label; + const input = document.createElement('input'); + input.type = 'color'; + input.value = colorHex(current, colorHex(spec?.default, '#2ee59a')); + field.append(span, input); + inputs.set(key, () => input.value); } else { const span = document.createElement('span'); span.textContent = label; diff --git a/public/style.css b/public/style.css index e5f52b9..f861d18 100644 --- a/public/style.css +++ b/public/style.css @@ -626,6 +626,27 @@ body.fullscreen #grid { padding-bottom: 0; } } .field.check input { width: 15px; height: 15px; accent-color: var(--accent); } +/* color fields: label on the left, swatch on the right */ +.field.color-field { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} +.field.color-field > span { flex: 1; min-width: 0; } +.field.color-field input[type="color"] { + width: 44px; + height: 26px; + padding: 2px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg); + cursor: pointer; +} +.field.color-field input[type="color"]:focus { outline: none; border-color: var(--accent); } +.field.color-field .field-help { flex-basis: 100%; } + /* ── modal dialogs (layout name prompt etc.) ───────────────────────── */ #modal-backdrop {