aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/widgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/widgets.js')
-rw-r--r--docs/prototypes/widgets.js147
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() {