aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/panel-widget-gallery.html44
-rw-r--r--docs/prototypes/widgets.js84
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}