diff options
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 53 | ||||
| -rw-r--r-- | docs/prototypes/widgets.js | 147 |
2 files changed, 157 insertions, 43 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 0065645..0e10631 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -1802,26 +1802,25 @@ card(M,'R17','Round CRT scope', /* ============ INDICATORS & READOUTS ============ */ const I=$('indicators'); card(I,'18','Status lamp', - `<span id="lamp18" style="display:inline-flex;gap:9px;align-items:center"><span class="lamp"></span><span class="lamp gold"></span><span class="lamp red"></span><span class="lamp off"></span><span class="lamp busy"></span></span>`, + (st,rd)=>GW.statusLamps(st,{onChange:(v,t)=>rd(t)}), '<b>one-glance health.</b> Green ok · gold engaged · red fail · dim off · pulsing busy. Click any lamp to cycle its state.'); card(I,'19','Badge / tag', - `<span id="badge19"><span class="badge">TUNNEL</span> <span class="badge red">LOW BATT</span> <span class="badge ghost">2.4G</span></span>`, + (st,rd)=>GW.badges(st,{onChange:(v,t)=>rd(t)}), '<b>a labelled flag.</b> MUTED, AIRPLANE, a band tag. Click a badge to cycle its variant.'); card(I,'20','Tabular readout', - `<div style="text-align:center"><div class="readout" id="treadout">24:10</div><span class="u">timer</span></div>`, + (st,rd)=>GW.tabularReadout(st,{onChange:(v,t)=>rd(t)}), '<b>a precise number.</b> Clock, countdown, volume %. Runs a live countdown; click to pause/resume.'); card(I,'21','Engraved label', - `<span class="engrave" id="engrave">outputs<span class="cnt" id="engc">· 3</span></span>`, + (st,rd)=>GW.engravedLabel(st,{onChange:(v,t)=>rd(t)}), '<b>section divider.</b> The hairline-flanked caps label with a count. Click to bump the count.'); card(I,'22','Output well', - `<div class="owell" id="owell"><div class="ostep"><span class="lamp"></span><span><b>Link</b><span class="ev">wlp170s0 · @Hyatt</span></span></div> - <div class="ostep"><span class="lamp gold"></span><span><b>DNS</b><span class="ev">resolving…</span></span></div></div>`, + (st,rd)=>GW.outputWell(st,{onChange:(v,t)=>rd(t)}), '<b>streaming step log.</b> Lamp-per-step with evidence. Click to stream the next step.'); card(I,'23','Toast / status line', - `<span class="toastw" id="toast">joined @Hyatt_WiFi — saved</span>`, + (st,rd)=>GW.toast(st,{onChange:(v,t)=>rd(t)}), '<b>transient confirmation.</b> The one-line result after an action. Click to fire the next toast.'); card(I,'26','Nixie tube', - `<span class="nixie" id="nixie"><span class="tube off"><b>0</b></span><span class="tube"><b>8</b></span></span>`, + (st,rd)=>GW.nixie(st,{onChange:(v,t)=>rd(t)}), '<b>a single warm-glowing numeral.</b> One lit digit per tube, leading zeros dark. Click to increment the count.'); card(I,'N20','Split-flap display', `<span class="flap" id="flap"><span class="flapd"><b>D</b></span><span class="flapd"><b>N</b></span><span class="flapd"><b>S</b></span></span>`, @@ -1901,35 +1900,7 @@ card(I,'R52','Blinkenlights front panel', /* 10-17, N11-N18 extracted to widgets.js (GW.*) — wired at their card records */ /* ============ INDICATOR WIRING ============ */ -/* 18 status lamp */ -(function(){const lst=['lamp','lamp gold','lamp red','lamp off','lamp busy'];const nm=['ok','engaged','fault','off','busy']; - $('lamp18').querySelectorAll('.lamp').forEach(l=>{l.style.cursor='pointer';l.onclick=()=>{ - let i=lst.indexOf(l.className);if(i<0)i=0;i=(i+1)%lst.length;l.className=lst[i];setRd('18',nm[i]);};});})(); -setRd('18','five states'); -/* 19 badge */ -(function(){const bv=['badge','badge red','badge ghost']; - $('badge19').querySelectorAll('.badge').forEach(b=>{b.style.cursor='pointer';b.onclick=()=>{ - let i=bv.indexOf(b.className);if(i<0)i=0;i=(i+1)%bv.length;b.className=bv[i];setRd('19',b.textContent);};});})(); -setRd('19','three variants'); -/* 21 engraved */ -let engN=3;$('engrave').onclick=()=>{engN=engN%9+1;$('engc').textContent='· '+engN;setRd('21','count '+engN);}; -setRd('21','count 3'); -/* 22 output well */ -const owSteps=[['gold','Probe','8.8.8.8 …'],['','Gateway','10.0.0.1 ok'],['','DNS','1.1.1.1 ok'],['red','Retry','timeout']];let owI=0; -$('owell').onclick=()=>{owI=(owI+1)%owSteps.length;const s=owSteps[owI];const d=document.createElement('div');d.className='ostep'; - d.innerHTML=`<span class="lamp ${s[0]}"></span><span><b>${s[1]}</b><span class="ev">${s[2]}</span></span>`; - $('owell').appendChild(d);while($('owell').children.length>5)$('owell').removeChild($('owell').firstChild);setRd('22','+ '+s[1]);}; -setRd('22','click to stream'); -/* 23 toast */ -const toastMsgs=['link up · 300 Mbps','bt paired — WH-1000XM4','profile switched','joined @Hyatt_WiFi — saved'];let toastN=0; -$('toast').onclick=()=>{toastN++;$('toast').textContent=toastMsgs[toastN%toastMsgs.length]; - $('toast').style.opacity='0';requestAnimationFrame(()=>{$('toast').style.transition='opacity .3s';$('toast').style.opacity='1';});setRd('23','fired '+toastN);}; -setRd('23','click to fire'); -/* 26 nixie */ -let nixV=8; -function setNix(v){nixV=v;const t=$('nixie').children,s=String(v).padStart(2,'0'); - t[0].classList.toggle('off',v<10);t[0].querySelector('b').textContent=s[0];t[1].querySelector('b').textContent=s[1];setRd('26',s);} -$('nixie').onclick=()=>setNix((nixV+1)%100);setNix(8); +/* 18-23, 26 extracted to widgets.js (GW.*) — wired at their card records */ /* N23 annunciator with alarm lifecycle: raise -> MSTR CAUTION flashes -> ACK steadies -> RESET clears */ (function(){const st=['acell','acell warn','acell fault'];const nm=['ok','warn','fault']; const mc=$('anmc');let acked=false; @@ -2273,12 +2244,8 @@ function renderSeven(){const mm=String(Math.floor(sevenSecs/60)).padStart(2,'0') function tickSeven(){sevenSecs=(sevenSecs-1+3600)%3600;renderSeven();} $('seven').onclick=()=>{sevenSecs=(sevenSecs+60)%3600;renderSeven();}; renderSeven(); -/* tabular timer */ -let tSecs=24*60+10,tRun=true; -function renderT(){$('treadout').textContent=String(Math.floor(tSecs/60)).padStart(2,'0')+':'+String(tSecs%60).padStart(2,'0'); - setRd('20',tRun?'running':'paused');} -function tickT(){if(tRun){tSecs=(tSecs-1+3600)%3600;renderT();}} -$('treadout').onclick=()=>{tRun=!tRun;renderT();};renderT(); +/* tabular timer: builder owns the countdown; the page keeps the 1s clock */ +function tickT(){$('card-20').gw.tick();} /* VFD marquee */ const vfdMsgs=['ARCHSETUP · NET OK · BT 2 · SND 62%','WIFI @Hyatt · 300 Mbps · VPN UP','BATTERY 84% · DISK 61% · TEMP 47C'];let vfdMi=0,vfdX=176; function tickVfd(){const t=$('vfdt');vfdX-=1.1;if(vfdX< -t.offsetWidth)vfdX=176;t.style.left=vfdX+'px';} diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index 89425f1..3d915de 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -2804,6 +2804,153 @@ GW.comfortMeter = function (host, opts = {}) { return { el: s, get: () => [vT, vH], set }; }; +/* ================= indicators & readouts ================= */ + +/* 18 status lamps — one lamp per health state; click any lamp to cycle it */ +GW.statusLamps = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const CLS = ['lamp', 'lamp gold', 'lamp red', 'lamp off', 'lamp busy']; + const NAMES = ['ok', 'engaged', 'fault', 'off', 'busy']; + const states = (opts.states || [0, 1, 2, 3, 4]).slice(); + const wrap = document.createElement('span'); + wrap.style.cssText = 'display:inline-flex;gap:9px;align-items:center'; + const lamps = states.map((st, i) => { + const l = document.createElement('span'); l.className = CLS[st]; l.style.cursor = 'pointer'; + l.addEventListener('click', () => set(i, states[i] + 1)); + wrap.appendChild(l); return l; + }); + host.appendChild(wrap); + function set(i, st) { + states[i] = ((st % CLS.length) + CLS.length) % CLS.length; + lamps[i].className = CLS[states[i]]; + onChange(states.slice(), NAMES[states[i]]); + } + onChange(states.slice(), states.length === 5 ? 'five states' : states.length + ' states'); + return { el: wrap, get: () => states.slice(), set }; +}; + +/* 19 badges — labelled flags; click a badge to cycle its variant */ +GW.badges = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const CLS = ['badge', 'badge red', 'badge ghost']; + const items = opts.items || [['TUNNEL', 0], ['LOW BATT', 1], ['2.4G', 2]]; + const vars = items.map(it => it[1]); + const wrap = document.createElement('span'); + const els = items.map(([txt, v], i) => { + const b = document.createElement('span'); b.className = CLS[v]; b.textContent = txt; b.style.cursor = 'pointer'; + b.addEventListener('click', () => set(i, vars[i] + 1)); + wrap.appendChild(b); + if (i < items.length - 1) wrap.appendChild(document.createTextNode(' ')); + return b; + }); + host.appendChild(wrap); + function set(i, v) { + vars[i] = ((v % CLS.length) + CLS.length) % CLS.length; + els[i].className = CLS[vars[i]]; + onChange(vars.slice(), items[i][0]); + } + onChange(vars.slice(), items.length === 3 ? 'three variants' : items.length + ' variants'); + return { el: wrap, get: () => vars.slice(), set }; +}; + +/* 20 tabular readout — mm:ss countdown; builder owns the state, the page + drives tick() on its own clock (tick contract); click pauses/resumes */ +GW.tabularReadout = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const wrap = document.createElement('div'); wrap.style.textAlign = 'center'; + wrap.innerHTML = '<div class="readout"></div><span class="u"></span>'; + const out = wrap.querySelector('.readout'); + wrap.querySelector('.u').textContent = opts.unit || 'timer'; + host.appendChild(wrap); + let secs, run = opts.run !== undefined ? !!opts.run : true; + const draw = () => { + out.textContent = String(Math.floor(secs / 60)).padStart(2, '0') + ':' + String(secs % 60).padStart(2, '0'); + onChange(secs, run ? 'running' : 'paused'); + }; + const set = v => { secs = ((v % 3600) + 3600) % 3600; draw(); }; + out.addEventListener('click', () => { run = !run; draw(); }); + set(opts.secs !== undefined ? opts.secs : 24 * 60 + 10); + return { el: wrap, get: () => secs, set, tick: () => { if (run) set(secs - 1); } }; +}; + +/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9 */ +GW.engravedLabel = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const e = document.createElement('span'); e.className = 'engrave'; + e.append(opts.label || 'outputs'); + const c = document.createElement('span'); c.className = 'cnt'; e.appendChild(c); + host.appendChild(e); + let n; + const set = v => { n = v; c.textContent = '· ' + n; onChange(n, 'count ' + n); }; + e.addEventListener('click', () => set(n % 9 + 1)); + set(opts.count !== undefined ? opts.count : 3); + return { el: e, get: () => n, set }; +}; + +/* 22 output well — streaming step log, lamp per step; click streams the next + demo step; push([lampCls, name, evidence]) appends programmatically */ +GW.outputWell = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const seed = opts.seed || [['', 'Link', 'wlp170s0 · @Hyatt'], ['gold', 'DNS', 'resolving…']]; + const steps = opts.steps || [['gold', 'Probe', '8.8.8.8 …'], ['', 'Gateway', '10.0.0.1 ok'], ['', 'DNS', '1.1.1.1 ok'], ['red', 'Retry', 'timeout']]; + const keep = opts.keep || 5; + const w = document.createElement('div'); w.className = 'owell'; host.appendChild(w); + const add = s => { + const d = document.createElement('div'); d.className = 'ostep'; + d.innerHTML = `<span class="lamp ${s[0]}"></span><span><b>${s[1]}</b><span class="ev">${s[2]}</span></span>`; + w.appendChild(d); while (w.children.length > keep) w.removeChild(w.firstChild); + }; + seed.forEach(add); + const push = s => { add(s); onChange(s, '+ ' + s[1]); }; + let i = 0; + w.addEventListener('click', () => { i = (i + 1) % steps.length; push(steps[i]); }); + onChange(null, 'click to stream'); + return { el: w, push }; +}; + +/* 23 toast — one-line transient confirmation; click fires the next demo + message; fire(msg) shows any message with the fade-in */ +GW.toast = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const msgs = opts.msgs || ['link up · 300 Mbps', 'bt paired — WH-1000XM4', 'profile switched', 'joined @Hyatt_WiFi — saved']; + const t = document.createElement('span'); t.className = 'toastw'; + t.textContent = opts.text || msgs[msgs.length - 1]; + host.appendChild(t); + const fire = msg => { + t.textContent = msg; t.style.opacity = '0'; + requestAnimationFrame(() => { t.style.transition = 'opacity .3s'; t.style.opacity = '1'; }); + }; + let n = 0; + t.addEventListener('click', () => { n++; fire(msgs[n % msgs.length]); onChange(n, 'fired ' + n); }); + onChange(0, 'click to fire'); + return { el: t, fire }; +}; + +/* 26 nixie tubes — one lit numeral per tube, leading zeros dark; click increments */ +GW.nixie = function (host, opts = {}) { + const onChange = opts.onChange || noop; + const digits = opts.digits || 2; + const max = Math.pow(10, digits); + const nx = document.createElement('span'); nx.className = 'nixie'; + for (let i = 0; i < digits; i++) { + const tu = document.createElement('span'); tu.className = 'tube'; tu.innerHTML = '<b></b>'; nx.appendChild(tu); + } + host.appendChild(nx); + let v; + const set = x => { + v = ((x % max) + max) % max; + const s = String(v).padStart(digits, '0'); + [...nx.children].forEach((tu, i) => { + tu.classList.toggle('off', i < digits - 1 && v < Math.pow(10, digits - 1 - i)); + tu.querySelector('b').textContent = s[i]; + }); + onChange(v, s); + }; + nx.addEventListener('click', () => set(v + 1)); + set(opts.value !== undefined ? opts.value : 8); + return { el: nx, get: () => v, set }; +}; + /* ---- widget CSS: injected once, grows as builders move in ---- */ const GW_CSS = ``; function ensureCss() { |
