diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-16 09:11:29 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-16 09:11:29 -0500 |
| commit | 6189466dd21f3e19b6bdae9bf74e38f6211ae542 (patch) | |
| tree | 0ca48eb533345dcd71a2e8933b21070cf1f5bd56 /docs/prototypes/widgets.js | |
| parent | 910993b25ba1736fc7f5958c53a7cdc523f98cf2 (diff) | |
| download | archsetup-6189466dd21f3e19b6bdae9bf74e38f6211ae542.tar.gz archsetup-6189466dd21f3e19b6bdae9bf74e38f6211ae542.zip | |
feat(gallery): rework the console-key and slide-toggle colour vocabulary
Console keys read SCAN before LIVE, and LIVE is green. The palette already defines --pass as "run lamps, gear greens, monitor bars, LIVE lamps", so the widget contradicted the language it draws from. Its note asserted the old colours too. The per-key `red` boolean became a `tone` field, since a boolean can't express three states.
The slide toggle gains red and warn on its `on` axis, so the engaged state can be the notable one (mute, record, airplane mode). Red was reachable only on the `off` axis before, which paints the disengaged state: the opposite claim. A new onText axis makes the ON ink reachable at all. `on` had bundled background and ink together, so "dark pill, green legend" couldn't be expressed.
Presets name a combination (panel, run, armed, caution, dark), which is what a consumer reaches for. The axes are how one gets built.
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 84 |
1 files changed, 73 insertions, 11 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index c12e5b7..147c782 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -126,30 +126,77 @@ GW.slideToggle = function (host, opts = {}) { if (!o) return; for (const [k, v] of Object.entries(o.vars)) sw.style.setProperty(k, v); }; - setStyle('on', opts.onStyle || 'amber'); - setStyle('off', opts.offStyle || 'dark'); - setStyle('offText', opts.offText || 'white'); - setStyle('thumb', opts.thumb || 'light'); + /* opts.preset names an intent; per-axis opts still win over it, so a caller can + take 'armed' and swap only the thumb. setPreset re-applies all four axes at + once and returns the axis map so a host UI can resync its own controls. */ + const preset = GW.slideToggle.PRESETS[opts.preset] || GW.slideToggle.PRESETS.panel; + /* AXIS_ORDER is load-bearing, not cosmetic: onText overrides the ink that the + `on` style sets, so applying the axes in any other order silently drops it. */ + const setPreset = name => { + const p = GW.slideToggle.PRESETS[name]; + if (!p) return null; + for (const axis of GW.slideToggle.AXIS_ORDER) if (p[axis]) setStyle(axis, p[axis]); + return p; + }; + const pick = { on: opts.onStyle, onText: opts.onText, off: opts.offStyle, + offText: opts.offText, thumb: opts.thumb }; + for (const axis of GW.slideToggle.AXIS_ORDER) setStyle(axis, pick[axis] || preset[axis]); let on; const set = v => { on = !!v; sw.classList.toggle('on', on); onChange(on, on ? 'ON' : 'OFF'); }; sw.addEventListener('click', () => set(!on)); set(opts.on !== undefined ? opts.on : true); - return { el: sw, get: () => on, set, setStyle }; + return { el: sw, get: () => on, set, setStyle, setPreset }; +}; +/* named presets — a combination that carries intent, which is what a consumer + actually reaches for; the STYLES axes below are how one is built. The `on` + tone does the semantic work and the thumb material backs it: a touchscreen + pill wears plastic, a run or armed switch wears metal. */ +GW.slideToggle.AXIS_ORDER = ['on', 'onText', 'off', 'offText', 'thumb']; +GW.slideToggle.PRESETS = { + panel: { on: 'amber', onText: 'panel', off: 'dark', offText: 'white', thumb: 'light' }, + run: { on: 'green', onText: 'panel', off: 'dark', offText: 'dim', thumb: 'chrome' }, + armed: { on: 'red', onText: 'cream', off: 'dark', offText: 'white', thumb: 'chrome' }, + caution: { on: 'warn', onText: 'panel', off: 'dark', offText: 'white', thumb: 'light' }, + /* dark — neither state lights the pill; the legend alone carries the state, + green for on and Control red ("OFF marks") for off. The chrome thumb stays + the position affordance, since with both pills dark it is the only one. */ + dark: { on: 'dark', onText: 'green', off: 'dark', offText: 'red', thumb: 'chrome' }, }; /* named styles per axis: dot = swatch color for pickers, vars = --sw-* overrides */ GW.slideToggle.STYLES = { on: { amber: { dot: 'var(--gold-hi)', vars: { '--sw-on-bg': 'linear-gradient(180deg,var(--amber-grad-top),var(--gold))', '--sw-on-brd': 'var(--gold-hi)', '--sw-on-ink': 'var(--panel)' } }, green: { dot: '#8fb944', vars: { '--sw-on-bg': 'linear-gradient(180deg,#a9c95f,var(--pass))', '--sw-on-brd': '#a9c95f', '--sw-on-ink': 'var(--panel)' } }, + /* red/warn make the ENGAGED state the notable one — mute, record, airplane + mode. Before them the only red was on the `off` axis, which paints the + disengaged state: the opposite claim. Palette semantics: --fail is + "muted keys", --amber-warn is "warn cells", distinct from panel amber. */ + red: { dot: 'var(--fail)', vars: { '--sw-on-bg': 'linear-gradient(180deg,#d98a6f,var(--fail))', '--sw-on-brd': 'var(--fail)', '--sw-on-ink': 'var(--cream)' } }, + warn: { dot: 'var(--amber-warn)', vars: { '--sw-on-bg': 'linear-gradient(180deg,#f7d193,var(--amber-warn))', '--sw-on-brd': 'var(--amber-warn)', '--sw-on-ink': 'var(--panel)' } }, dark: { dot: '#242019', vars: { '--sw-on-bg': 'linear-gradient(180deg,#15130f,#242019)', '--sw-on-brd': 'var(--slate)', '--sw-on-ink': 'var(--cream)' } }, }, off: { dark: { dot: '#242019', vars: { '--sw-off-bg': 'linear-gradient(180deg,#15130f,#242019)', '--sw-off-brd': 'var(--slate)' } }, red: { dot: 'var(--fail)', vars: { '--sw-off-bg': 'linear-gradient(180deg,#d98a6f,var(--fail))', '--sw-off-brd': 'var(--fail)' } }, }, + /* onText overrides the ink the `on` style sets. The two are not independent — + ink must contrast with its own background, which is why each `on` atom still + carries a sensible default; naming onText is how a caller says "keep the + pill, change the legend". That is what makes the `dark` preset possible: + both states unlit, the legend colour carrying the state on its own. */ + onText: { + panel: { dot: 'var(--panel)', vars: { '--sw-on-ink': 'var(--panel)' } }, + cream: { dot: 'var(--cream)', vars: { '--sw-on-ink': 'var(--cream)' } }, + green: { dot: 'var(--sevgrn)', vars: { '--sw-on-ink': 'var(--sevgrn)' } }, + }, offText: { white: { dot: 'var(--cream)', vars: { '--sw-off-ink': 'var(--cream)' } }, + /* Control red — the palette's "OFF marks" ink, so a red OFF legend is the + kit's existing grammar rather than a new claim. */ red: { dot: '#e0523a', vars: { '--sw-off-ink': '#e0523a' } }, + /* steel is the kit's engraved-caption ink: lets an off toggle recede where a + panel carries many of them and eight cream OFFs would all shout equally. */ + dim: { dot: 'var(--steel)', vars: { '--sw-off-ink': 'var(--steel)' } }, black: { dot: '#14110e', vars: { '--sw-off-ink': '#14110e' } }, }, thumb: { @@ -160,10 +207,14 @@ GW.slideToggle.STYLES = { }, }; -/* 02 console keys — mutually exclusive push buttons; {label, red} per key */ +/* 02 console keys — mutually exclusive push buttons; {label, tone} per key. + tone picks the engaged look: undefined = gold (the default lit key), 'green' = + run green, 'red' = terracotta. Reading order runs safe -> live -> muted, and + LIVE takes green because that is what --pass means everywhere else in the kit + (the palette names it "run lamps, gear greens, monitor bars, LIVE lamps"). */ GW.consoleKeys = function (host, opts = {}) { const onChange = opts.onChange || noop; - const keys = opts.keys || [{ label: 'LIVE' }, { label: 'SCAN' }, { label: 'MUTED', red: true }]; + const keys = opts.keys || GW.consoleKeys.DEFAULT_KEYS; const wrap = document.createElement('span'); host.appendChild(wrap); const btns = keys.map(k => { const b = document.createElement('button'); b.className = 'key'; b.textContent = k.label; @@ -171,14 +222,24 @@ GW.consoleKeys = function (host, opts = {}) { }); let idx; const set = i => { - idx = i; - btns.forEach((b, j) => { b.classList.remove('on', 'red'); if (j === i) b.classList.add(keys[i].red ? 'red' : 'on'); }); - onChange(i, keys[i].label); + idx = Math.max(0, Math.min(keys.length - 1, i)); + btns.forEach((b, j) => { + b.classList.remove('on', 'green', 'red'); + if (j === idx) b.classList.add(keys[idx].tone || 'on'); + }); + onChange(idx, keys[idx].label); }; btns.forEach((b, i) => b.addEventListener('click', () => set(i))); - set(opts.active || 0); + /* The default set engages LIVE, not the first key: it is the meaningful + resting state for a console and it shows the green. A caller supplying its + own keys gets the first one unless it says otherwise. */ + set(opts.active !== undefined ? opts.active + : (keys === GW.consoleKeys.DEFAULT_KEYS ? 1 : 0)); return { el: wrap, get: () => idx, set }; }; +GW.consoleKeys.DEFAULT_KEYS = [ + { label: 'SCAN' }, { label: 'LIVE', tone: 'green' }, { label: 'MUTED', tone: 'red' }, +]; /* 03 horizontal fader — continuous 0-100 */ GW.faderH = function (host, opts = {}) { @@ -4011,6 +4072,7 @@ const GW_CSS = ` .key:hover{color:var(--gold);border-color:var(--gold)} .key:active{transform:translateY(1px)} .key.on{color:var(--panel);background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));border-color:var(--gold-hi);font-weight:700} +.key.green{color:var(--cream);background:linear-gradient(180deg,#9cbf5e,var(--pass));border-color:var(--pass);font-weight:700} .key.red{color:var(--cream);background:linear-gradient(180deg,#d98a6f,var(--fail));border-color:var(--fail)} .key.off{opacity:.4} |
