diff options
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 147 |
1 files changed, 147 insertions, 0 deletions
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() { |
