aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes')
-rw-r--r--docs/prototypes/widgets.js164
1 files changed, 129 insertions, 35 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js
index dc37bd4..bd5c79f 100644
--- a/docs/prototypes/widgets.js
+++ b/docs/prototypes/widgets.js
@@ -3746,7 +3746,17 @@ DUPRE.edgeMeter = function (host, opts = {}) {
};
/* R17 round CRT scope — pale phosphor face; the trace is the instrument's own
- animation, so it lives here with its reduced-motion gate */
+ animation, so it lives here with its reduced-motion gate.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(amp, 'Vpp N') fires on every animation tick, including
+ the initial paint — the demo waveform is instrument-owned.
+ handle: el, tick() — one animation step (the reduced-motion path paints
+ once and never ticks again on its own).
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate, except the
+ face gradient, which reads screen-family vars from this instrument's
+ subtree and so lives in a local defs. */
DUPRE.roundCrt = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 110, 104);
@@ -3797,7 +3807,16 @@ DUPRE.roundCrt = function (host, opts = {}) {
return { el: s, tick };
};
-/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag */
+/* R43 attitude indicator — sky/ground roll+shift behind a fixed miniature aircraft; 2D drag.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: bank (initial degrees, -60..60, default 0); pitch (initial
+ degrees, -20..20, default 0); onChange({bank, pitch}, 'BANK …
+ · PITCH …') fires on every set, including the initial paint.
+ handle: el, get() ({bank, pitch}), set(bank, pitch) — both clamp to
+ their ranges; the stage itself is a 2D drag surface.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.attitudeIndicator = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 130);
@@ -3853,7 +3872,16 @@ DUPRE.attitudeIndicator = function (host, opts = {}) {
};
/* R44 heading bug + servo needle — drag parks the command; the needle chases
- with honest servo lag (instrument-owned animation, reduced-motion gated) */
+ with honest servo lag (instrument-owned animation, reduced-motion gated).
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (initial commanded heading in degrees, default 90 —
+ normalized to 0..360); onChange({cmd, act}, 'CMD … · ACT …')
+ fires on every set and on every servo step.
+ handle: el, get() ({cmd, act}), set(v) — v normalizes to 0..360;
+ dragging the stage moves the command bug, the needle chases.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.headingBug = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 130);
@@ -3884,7 +3912,7 @@ DUPRE.headingBug = function (host, opts = {}) {
fill: '#bfc4d0', stroke: '#4a4e58', 'stroke-width': .6
});
svgEl(s, 'circle', { cx, cy, r: 4.5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 });
- let cmd = opts.value ?? 90, act = cmd;
+ let cmd = ((opts.value ?? 90) % 360 + 360) % 360, act = cmd;
const draw = () => {
bug.setAttribute('transform', `rotate(${cmd.toFixed(1)},${cx},${cy})`);
needle.setAttribute('transform', `rotate(${act.toFixed(1)},${cx},${cy})`);
@@ -3904,7 +3932,15 @@ DUPRE.headingBug = function (host, opts = {}) {
};
/* R53 circular chart recorder — a day per revolution; the pen draws the cycle
- (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper */
+ (instrument-owned clock, reduced-motion paints the full day once); click for fresh paper.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: onChange(hour, 'HH:MM · N%') fires on every pen step — the demo
+ day-cycle signal is instrument-owned.
+ handle: el, get() (current hour, 0..24), reset() — fresh paper, pen to
+ 00:00; clicking the stage does the same.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.chartRecorder = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg press', 130, 130);
@@ -3954,7 +3990,16 @@ DUPRE.chartRecorder = function (host, opts = {}) {
return { el: s, get: () => hour, reset };
};
-/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive */
+/* R54 vertical tape instrument — the scale scrolls behind a fixed index; drag to drive.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: value (initial units, 5..35 on the printed RPM x100 scale,
+ default 24); onChange(value, 'RPM N') fires on every set,
+ including the initial paint.
+ handle: el, get() (units), set(v) — v clamps 5..35; the stage drags
+ vertically.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.verticalTape = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 70, 130);
@@ -3996,7 +4041,16 @@ DUPRE.verticalTape = function (host, opts = {}) {
return { el: s, get: () => v, set };
};
-/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles */
+/* R55 twin-needle gauge — mirrored half-scales, one hub, two independent needles.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: fuel (initial, 0..4, default 2.4); oil (initial, 0..4, default
+ 3.1); onChange([fuel, oil], 'FUEL n.n · OIL n.n') fires on every
+ set, including the initial paint.
+ handle: el, get() ([fuel, oil]), set(fuel, oil) — both clamp 0..4;
+ each half of the stage drags its own needle vertically.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.twinNeedle = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 120, 110);
@@ -4045,7 +4099,7 @@ DUPRE.twinNeedle = function (host, opts = {}) {
};
const nF = needle('#e0523a'), nO = needle('#bfc4d0');
svgEl(s, 'circle', { cx, cy, r: 5, fill: '#3a3631', stroke: '#060505', 'stroke-width': 1 });
- let vF = opts.fuel ?? 2.4, vO = opts.oil ?? 3.1;
+ let vF, vO;
const draw = () => {
nF.setAttribute('transform', `rotate(${angL(vF).toFixed(1)},${cx},${cy})`);
nO.setAttribute('transform', `rotate(${angR(vO).toFixed(1)},${cx},${cy})`);
@@ -4060,11 +4114,21 @@ DUPRE.twinNeedle = function (host, opts = {}) {
};
half(0, () => vF, v => vF = v);
half(60, () => vO, v => vO = v);
- draw();
+ set(opts.fuel ?? 2.4, opts.oil ?? 3.1);
return { el: s, get: () => [vF, vO], set };
};
-/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts */
+/* R56 comfort-zone crossed needles — temp and humidity cross over printed verdicts.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: temp (initial °F, 40..100, default 72); humidity (initial %,
+ 0..100, default 45); onChange([temp, humidity], 'NF · N% RH ·
+ VERDICT') fires on every set, including the initial paint.
+ handle: el, get() ([temp, humidity]), set(temp, humidity) — both clamp
+ to their scales; the left half of the stage drags temp, the
+ right half humidity.
+ SVG-built: styling is inline attributes plus the shared .rsvg stage block
+ of DUPRE_CSS; gradients register in the shared defs plate. */
DUPRE.comfortMeter = function (host, opts = {}) {
const onChange = opts.onChange || noop;
const s = stageSvg(host, 'rsvg', 130, 122);
@@ -4137,7 +4201,7 @@ DUPRE.comfortMeter = function (host, opts = {}) {
const nT = needle(), nH = needle();
nT.setAttribute('transform', `translate(${PT[0]},${PT[1]})`);
nH.setAttribute('transform', `translate(${PH[0]},${PH[1]})`);
- let vT = opts.temp ?? 72, vH = opts.humidity ?? 45;
+ let vT, vH;
const verdict = () => vT > 78 ? 'warm' : vT < 62 ? 'cold' : vH > 60 ? 'humid' : vH < 30 ? 'dry' : 'right';
const draw = () => {
nT.setAttribute('transform', `translate(${PT[0]},${PT[1]}) rotate(${angT(vT).toFixed(1)})`);
@@ -4154,7 +4218,7 @@ DUPRE.comfortMeter = function (host, opts = {}) {
};
half(0, () => vT, v => vT = v, 40, 100);
half(65, () => vH, v => vH = v, 0, 100);
- draw();
+ set(opts.temp ?? 72, opts.humidity ?? 45);
return { el: s, get: () => [vT, vH], set };
};
@@ -4192,15 +4256,25 @@ DUPRE.statusLamps = function (host, opts = {}) {
return { el: wrap, get: () => states.slice(), set };
};
-/* 19 badges — labelled flags; click a badge to cycle its variant */
+/* 19 badges — labelled flags; click a badge to cycle its variant.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: items (array of [label, variant] pairs, variant 0 gold · 1 red ·
+ 2 ghost, default a three-badge demo set); onChange(variants array
+ copy, label) fires per change and once at build with a count
+ summary.
+ handle: el, get() (variants copy), set(i, v) — v wraps modulo 3.
+ CSS lives in the .dupre-badge block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.badges = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const CLS = ['badge', 'badge red', 'badge ghost'];
+ const CLS = ['dupre-badge', 'dupre-badge dupre-red', 'dupre-badge dupre-ghost'];
+ const norm = v => (((v | 0) % CLS.length) + CLS.length) % CLS.length;
const items = opts.items || [['TUNNEL', 0], ['LOW BATT', 1], ['2.4G', 2]];
- const vars = items.map(it => it[1]);
+ const vars = items.map(it => norm(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';
+ const els = items.map(([txt], i) => {
+ const b = document.createElement('span'); b.className = CLS[vars[i]]; 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(' '));
@@ -4208,7 +4282,7 @@ DUPRE.badges = function (host, opts = {}) {
});
host.appendChild(wrap);
function set(i, v) {
- vars[i] = ((v % CLS.length) + CLS.length) % CLS.length;
+ vars[i] = norm(v);
els[i].className = CLS[vars[i]];
onChange(vars.slice(), items[i][0]);
}
@@ -4217,13 +4291,24 @@ DUPRE.badges = function (host, opts = {}) {
};
/* 20 tabular readout — mm:ss countdown; builder owns the state, the page
- drives tick() on its own clock (tick contract); click pauses/resumes */
+ drives tick() on its own clock (tick contract); click pauses/resumes.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: secs (initial seconds, wraps modulo 3600, default 24:10); run
+ (initial running state, default true); unit (caption under the
+ digits, default 'timer'); onChange(secs, 'running'/'paused')
+ fires on every set, including the initial paint.
+ handle: el, get() (secs), set(v) — v wraps modulo 3600; tick() — one
+ second down when running (the page's clock drives it); clicking
+ the digits pauses/resumes.
+ CSS lives in the .dupre-readout block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.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';
+ wrap.innerHTML = '<div class="dupre-readout"></div><span class="dupre-unit"></span>';
+ const out = wrap.querySelector('.dupre-readout');
+ wrap.querySelector('.dupre-unit').textContent = opts.unit || 'timer';
host.appendChild(wrap);
let secs, run = opts.run !== undefined ? !!opts.run : true;
const draw = () => {
@@ -4236,12 +4321,21 @@ DUPRE.tabularReadout = function (host, opts = {}) {
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 */
+/* 21 engraved label — hairline-flanked caps label with a count; click bumps 1-9.
+
+ Contract (everything a consumer needs; no page globals touched):
+ opts: label (caption text, default 'outputs'); count (initial count,
+ default 3); onChange(count, 'count N') fires on every set,
+ including the initial paint.
+ handle: el, get() (count), set(v); clicking the label cycles the count
+ 1..9.
+ CSS lives in the .dupre-engrave block of DUPRE_CSS; no other styles
+ involved. */
DUPRE.engravedLabel = function (host, opts = {}) {
const onChange = opts.onChange || noop;
- const e = document.createElement('span'); e.className = 'engrave';
+ const e = document.createElement('span'); e.className = 'dupre-engrave';
e.append(opts.label || 'outputs');
- const c = document.createElement('span'); c.className = 'cnt'; e.appendChild(c);
+ const c = document.createElement('span'); c.className = 'dupre-cnt'; e.appendChild(c);
host.appendChild(e);
let n;
const set = v => { n = v; c.textContent = '· ' + n; onChange(n, 'count ' + n); };
@@ -5420,9 +5514,9 @@ const DUPRE_CSS = `
.dupre-chip{color:var(--dim);cursor:pointer;border-bottom:1px dotted var(--wash);font-size:12px}
.dupre-chip.dupre-on{color:var(--chip-on,var(--gold));border-color:var(--chip-on,var(--gold))}
-.badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
-.badge.red{background:var(--fail);color:var(--cream)}
-.badge.ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
+.dupre-badge{font-size:.62rem;letter-spacing:.18em;color:var(--panel);background:var(--gold);border-radius:4px;padding:1px 6px}
+.dupre-badge.dupre-red{background:var(--fail);color:var(--cream)}
+.dupre-badge.dupre-ghost{background:transparent;border:1px solid var(--slate);color:var(--silver)}
/* fader */
.dupre-fader{width:150px;height:16px;position:relative;cursor:pointer;touch-action:none}
@@ -5489,9 +5583,9 @@ const DUPRE_CSS = `
.dupre-ring b{position:relative;color:var(--cream);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums}
/* tabular readout */
-.readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer}
-.readout small{color:var(--dim);font-size:12px;font-weight:400}
-.readout .u{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
+.dupre-readout{color:var(--cream);font-size:24px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:.04em;cursor:pointer}
+.dupre-readout small{color:var(--dim);font-size:12px;font-weight:400}
+.dupre-unit{color:var(--steel);font-size:.6rem;letter-spacing:.2em;display:block;text-align:center;margin-top:2px}
/* sparkline */
.dupre-spark{width:170px;height:44px}
@@ -5515,11 +5609,11 @@ const DUPRE_CSS = `
.dupre-seg button.dupre-on{background:var(--seg-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));color:var(--seg-on-ink,var(--panel));font-weight:700}
/* engraved section label */
-.engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
+.dupre-engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer;
display:flex;align-items:center;gap:9px}
-.engrave::before,.engrave::after{content:"";height:1px;background:var(--wash);flex:1}
-.engrave::before{max-width:10px}
-.engrave .cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
+.dupre-engrave::before,.dupre-engrave::after{content:"";height:1px;background:var(--wash);flex:1}
+.dupre-engrave::before{max-width:10px}
+.dupre-engrave .dupre-cnt{color:var(--dim);letter-spacing:.1em;text-transform:none}
/* waveform strip */
.dupre-wave{width:170px;height:38px}