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 | |
| 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')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 44 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 84 |
2 files changed, 111 insertions, 17 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 2a2910b..e64ddd3 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -1187,7 +1187,7 @@ card(C,'01','Slide toggle', '<b>on / off, the touchscreen way.</b> The one born-digital control in the kit — the smartphone pill toggle the live net/bt/audio panels use. Click to flip; readout shows the state. Physical cousins: the bat-handle (R03) and the rocker (N01).'); card(C,'02','Console key', (st,rd)=>GW.consoleKeys(st,{onChange:(i,t)=>rd(t)}), - '<b>physical push button.</b> Mutually exclusive — click one to engage. Gold = live, terracotta = muted.'); + '<b>physical push button.</b> Mutually exclusive — click one to engage. Green = live, terracotta = muted, gold = any other engaged key.'); card(C,'03','Horizontal fader', (st,rd)=>GW.faderH(st,{value:68,onChange:(v,t)=>rd(t)}), '<b>continuous 0-100.</b> Per-device volume, brightness. Drag the gold cap; readout tracks.'); @@ -1657,25 +1657,57 @@ BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no); m=>ids.has(m)?`<a class="xref" href="#card-${m}">${m}</a>`:m);});})(); /* style chips: shared demo rig for builders with constructor style opts — - reads a builder's STYLES table and drives setStyle on the live card instance */ -function styleChips(no,STYLES,AXES){ + reads a builder's STYLES table and drives setStyle on the live card instance. + PRESETS (optional) adds a leading group of named combinations: a preset chip + applies all axes at once and resyncs the axis chips beneath it, and changing + any axis afterwards clears the preset — the chips must never claim a preset + the widget has since diverged from. */ +function styleChips(no,STYLES,AXES,PRESETS,defPreset){ const h=$('card-'+no)?.gw; if(!h)return; const cardEl=h.el.closest('.card'); const row=document.createElement('div'); row.className='famchips'; + const sel={}, sync={}, groups=[]; let clearPreset=()=>{}; + /* Re-apply every axis on any change rather than just the one clicked: axes are + not independent (onText overrides the ink `on` sets), so a lone setStyle can + drop a selection the chips still show as lit — the card would then display a + combination the widget isn't in. Applying all of them in AXES order makes + the outcome independent of click order. */ + const applyAll=()=>{for(const [,axis] of AXES) if(sel[axis]) h.setStyle(axis,sel[axis]);}; for(const [label,axis,def] of AXES){ + sel[axis]=def; const g=document.createElement('span'); g.className='fgroup'; const lab=document.createElement('span'); lab.className='lab'; lab.textContent=label; g.appendChild(lab); const chips=[]; for(const [name,o] of Object.entries(STYLES[axis])){ const b=document.createElement('span'); b.className='fc'+(name===def?' on':''); b.style.background=o.dot; b.title=name; chips.push(b); - b.addEventListener('click',()=>{h.setStyle(axis,name); - chips.forEach(x=>x.classList.toggle('on',x===b));}); + b.addEventListener('click',()=>{sel[axis]=name; applyAll(); + chips.forEach(x=>x.classList.toggle('on',x===b)); clearPreset();}); g.appendChild(b);} + sync[axis]=name=>{sel[axis]=name; chips.forEach(x=>x.classList.toggle('on',x.title===name));}; + groups.push(g);} + if(PRESETS&&h.setPreset){ + const g=document.createElement('span'); g.className='fgroup'; + const lab=document.createElement('span'); lab.className='lab'; lab.textContent='preset'; g.appendChild(lab); + const chips=[]; + for(const [name,p] of Object.entries(PRESETS)){ + const b=document.createElement('span'); b.className='fc'+(name===defPreset?' on':''); b.title=name; + /* the on-tone is the preset's identity, so it doubles as the swatch */ + b.style.background=STYLES.on[p.on].dot; chips.push(b); + b.addEventListener('click',()=>{ + const applied=h.setPreset(name); if(!applied)return; + chips.forEach(x=>x.classList.toggle('on',x===b)); + for(const [axis,style] of Object.entries(applied)) sync[axis]?.(style);}); + g.appendChild(b);} + clearPreset=()=>chips.forEach(x=>x.classList.remove('on')); row.appendChild(g);} + groups.forEach(g=>row.appendChild(g)); cardEl.querySelector('.opts').appendChild(row); } -styleChips('01',GW.slideToggle.STYLES,[['on','on','amber'],['off','off','dark'],['off text','offText','white'],['thumb','thumb','light']]); +styleChips('01',GW.slideToggle.STYLES, + [['on','on','amber'],['on text','onText','panel'],['off','off','dark'], + ['off text','offText','white'],['thumb','thumb','light']], + GW.slideToggle.PRESETS,'panel'); styleChips('R05',GW.filterBank.STYLES,[['panel','panel','silver'],['shape','caps','block'],['color','capColor','black']]); styleChips('06',GW.segmented.STYLES,[['accent','accent','amber']]); styleChips('25',GW.slideRule.STYLES,[['face','skin','warm']]); 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} |
