aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/panel-widget-gallery.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
-rw-r--r--docs/prototypes/panel-widget-gallery.html134
1 files changed, 126 insertions, 8 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html
index 9740c46..25b10c6 100644
--- a/docs/prototypes/panel-widget-gallery.html
+++ b/docs/prototypes/panel-widget-gallery.html
@@ -126,6 +126,10 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1}
background:#221f1b;border:1px solid #33302b}
.vtally .vn{margin-left:auto;color:var(--cream);font-variant-numeric:tabular-nums}
.vtally .vtot{border-top:1px solid var(--wash);margin-top:3px;padding-top:4.5px;color:var(--steel)}
+.vtally .vcopy{margin-top:5px;width:100%;font:inherit;font-size:.7rem;letter-spacing:.04em;
+ background:transparent;color:var(--steel);border:1px solid var(--wash);border-radius:3px;
+ padding:2.5px 0;cursor:pointer}
+.vtally .vcopy:hover{color:var(--gold-hi);border-color:var(--gold)}
.stagew{background:var(--well);border:1px solid #201d17;border-radius:9px;padding:14px 12px;
min-height:78px;display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap}
.wrd{color:var(--gold-hi);font-size:.8rem;letter-spacing:.06em;font-variant-numeric:tabular-nums;
@@ -680,6 +684,14 @@ const INFO={
origin:'PDP-11/70 and IMSAI front panels.',difficulty:'Intuitive to watch, expert to read.',
prefer:'A spinner says busy; blinkenlights say HOW busy and with what rhythm.',
period:'1965-80 minicomputer era; the icon of computing at work.'},
+'R57':{input:'Click keys to type, or click the plate and type on the keyboard (letters, digits, space, Backspace, Enter). DEL takes back one character, CLR wipes the lot, ENT commits.',
+ solves:'Free alphanumeric text on a panel that has no keyboard — the one job the rest of the kit cannot do.',
+ use:'Specialty. Shines where a panel must accept a string it could not know in advance: a passphrase, an SSID, a callsign, a label.',
+ limits:'Clicking is slow by design: a 20-character passphrase is 20 clicks, which is why the plate also takes the keyboard when focused. It is for panels with no keyboard of their own, and a real one still beats it for volume.',
+ origin:'Fleet, kiosk and access-control keypads — a membrane plate for the colour-coded function keys, a stainless one for the letters-left layout and the backspace.',
+ difficulty:'Intuitive to use. Clicking is tedious past a few words, which is what the keyboard is for.',
+ prefer:'Text is genuinely unbounded. R16 is the digits-only sibling and is faster where the alphabet is not needed.',
+ ref:'../../working/retro-stereo-widgets/references/2026-07-16-abc-keypad-membrane-color.png'},
'R56':{input:'Drag the left half for temperature, the right for humidity. Drag-only, two surfaces.',
solves:'A categorical verdict from two values — the crossing point falls into a printed judgment.',
use:'Common on weather stations and comfort meters. Shines when the pair only matters as a combined condition: comfort, safe-operating region, duty envelope.',
@@ -1039,6 +1051,68 @@ const INFO={
const INFO_FIELDS=[['input','input'],['solves','solves'],['use','use'],['limits','limits'],
['origin','origin'],['difficulty','difficulty'],['prefer','prefer when'],['period','period']];
const VLAMP_TITLES={off:'validation: not done',amber:'validation: in progress',green:'validation: done'};
+/* VSTATUS — the baked validation record: the durable half of the walk.
+ Lamp clicks write localStorage, which is per-browser-profile and dies with a
+ cache clear, so the walk's progress is baked back into source periodically
+ (the "copy for source" control under the tally emits this block; paste it
+ here). Only amber/green are listed — off is the default and stays implicit.
+ Precedence is localStorage first, VSTATUS second: the live walk wins on the
+ machine doing it, and the baked record fills in on a fresh profile, after a
+ clear, or on the other daily driver. 'off' is a stored value like any other,
+ so deliberately un-checking a card beats a baked green rather than reverting
+ to it on reload. */
+const VSTATUS={
+ "01": "green",
+ "06": "green",
+ "25": "green",
+ "R05": "green",
+ "12": "green",
+ "R07": "amber",
+ "18": "green",
+ "R10": "green",
+ "R11": "green",
+ "R52": "amber"
+};
+/* GVexport — the live lamp state in VSTATUS shape, ready to paste back above.
+ Reads the lamps rather than localStorage so what you copy is what you see.
+ Builds the text by hand rather than via JSON.stringify: stringify orders
+ canonical integer keys ('12') numerically ahead of everything else ('01' has
+ a leading zero, 'R05' isn't numeric), which would reshuffle the baked block
+ on every bake and bury each walk's real change in diff noise. Card order is
+ DOM order, so the block grows in place. */
+function GVexport(){
+ const rows=[];
+ document.querySelectorAll('.vlamp').forEach(l=>{
+ const v=l.dataset.v;
+ if(v!=='off') rows.push(' "'+l.closest('.card').id.slice(5)+'": "'+v+'"');});
+ return '{\n'+rows.join(',\n')+'\n}';
+}
+/* Hand the export over selected, and let the reader press Ctrl+C themselves.
+ The page deliberately never tries to copy on their behalf, because from a
+ file:// origin neither programmatic path works here (Chrome 150, 2026-07-16):
+ - navigator.clipboard.writeText rejects NotAllowedError, "Write permission
+ denied" — a file origin can't hold the clipboard-write permission.
+ - execCommand('copy') returns TRUE and writes nothing. Worse than failing,
+ because the return value invites the page to claim success it didn't have.
+ A hand-typed Ctrl+C works fine, so this is specific to a page copying on the
+ user's behalf, NOT a broken clipboard: a Wayland-set clipboard crosses to X11
+ and Emacs reads it correctly (sentinel-verified). Selecting the text is
+ therefore the whole job — the user's own copy is the one that works, and it
+ lands in the real clipboard where every app can reach it.
+ Do NOT "improve" this by reading the X/Wayland PRIMARY selection instead:
+ PRIMARY holds whatever was last selected ANYWHERE, so it silently returns
+ unrelated content (it produced a private SMS while this was being built). */
+function gvCopy(btn){
+ const flash=m=>{btn.textContent=m;setTimeout(()=>btn.textContent='copy for source',2600);};
+ const ta=document.createElement('textarea');
+ ta.value=GVexport();
+ ta.style.cssText='position:fixed;left:12px;bottom:12px;z-index:70;width:320px;height:170px;'+
+ 'font:12px ui-monospace,monospace;background:#14120f;color:#e8dcc0;border:1px solid #7d5c16;'+
+ 'border-radius:4px;padding:6px';
+ document.body.appendChild(ta); ta.select();
+ flash('press Ctrl+C ▸');
+ ta.addEventListener('blur',()=>ta.remove());
+}
/* index tally: recounts every card lamp; setV calls it on every state change.
Clicking a row jumps to the next card in that state (cycles), so a count
that disagrees with a visual scan can be audited card by card. */
@@ -1052,10 +1126,12 @@ function updateVTally(){
`<div class="vrow" data-v="green"><span class="vdot" data-v="green"></span>done<span class="vn">${n.green}</span></div>`+
`<div class="vrow" data-v="amber"><span class="vdot" data-v="amber"></span>in progress<span class="vn">${n.amber}</span></div>`+
`<div class="vrow" data-v="off"><span class="vdot"></span>not done<span class="vn">${n.off}</span></div>`+
- `<div class="vrow vtot">total<span class="vn">${lamps.length}</span></div>`;
+ `<div class="vrow vtot">total<span class="vn">${lamps.length}</span></div>`+
+ `<button class="vcopy" type="button">copy for source</button>`;
el.querySelectorAll('.vrow[data-v]').forEach(row=>row.addEventListener('click',()=>{
VJUMP[row.dataset.v]=0; auditNext(row.dataset.v);
}));
+ el.querySelector('.vcopy').addEventListener('click',e=>gvCopy(e.target));
}
/* audit stepper: a row click jumps to the first card in that state and docks a
floating pill; clicking the pill advances through the rest without scrolling
@@ -1090,7 +1166,7 @@ function card(host, no, name, html, note){
`<div class="opts"></div><div class="wnote">${note}</div>`;
const lamp=c.querySelector('.vlamp'), VKEY='gv-'+no, VSTATES=['off','amber','green'];
const setV=v=>{lamp.dataset.v=v;lamp.title=VLAMP_TITLES[v];updateVTally();};
- setV(localStorage.getItem(VKEY)||'off');
+ setV(localStorage.getItem(VKEY)||VSTATUS[no]||'off');
lamp.addEventListener('click',e=>{e.stopPropagation();
const v=VSTATES[(VSTATES.indexOf(lamp.dataset.v)+1)%3];
setV(v);localStorage.setItem(VKEY,v);});
@@ -1115,7 +1191,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.');
@@ -1273,6 +1349,13 @@ card(C,'R50','Two-hand safety control',
card(C,'R51','Voice-loop keyset',
(st,rd)=>GW.voiceLoop(st,{onChange:(v,t)=>rd(t)}),
'<b>attention management, not selection.</b> Each loop is independent: click to monitor (green bar, flickering with activity), again to talk (amber, exclusive — one voice out), again to drop. Flight controllers ran a dozen of these at once. After a mission-control comm keyset.');
+card(C,'R57','ABC entry keypad',
+ (st,rd)=>GW.abcKeypad(st,{onChange:(v,t)=>rd(t)}),
+ '<b>letters on a faceplate.</b> A-Z laid out alphabetically, not QWERTY — the industrial convention wherever the operator is not assumed to touch-type. Click the keys, or click the plate and type: it takes the keyboard once focused, and every key routes through the same handle a click does. DEL takes back one character, CLR wipes the lot, ENT commits. R16 enters digits; this is its alphanumeric sibling, and the only card in the kit that takes free text. After fleet and kiosk keypads: the membrane plate\'s colour-coded CLR / ENT, the stainless plate\'s letters-left arrangement and its backspace. Its CANCEL is dropped — that plate is a whole terminal with a transaction to abandon, where this is one control in a panel that owns its own dismiss.');
+
+card(C,'R58','Index typewriter',
+ (st,rd)=>GW.indexPlate(st,{onChange:(v,t)=>rd(t)}),
+ '<b>type without a keyboard.</b> One hand walks the stylus over a printed plate of characters, the other pulls the lever, and only the lever prints — hunt as long as you like and nothing happens until the second hand moves. Select and commit on two separate controls, which no other card in the kit does. Both cases live on the plate, so there is no shift key, and it carries what a keyboard skips: accents, fractions, the section mark. After the AEG Mignon Model 4 (1924), whose frequency-ordered keys we did not keep — reading the plate IS the interaction, so ours is alphabetical.');
/* ============ METERS & GAUGES ============ */
const M=$('meters');
@@ -1574,6 +1657,9 @@ screenChips('R17',$('card-R17').gw.el,['green','amber','red','blue','vfd'],'gree
screenChips('R19',$('card-R19').gw.el,['white','green','amber','blue','vfd'],'white');
screenChips('R31',$('card-R31').gw.el,['amber','green','red','blue','vfd'],'amber');
screenChips('N11',$('card-N11').gw.el,['green','amber','red','blue','vfd'],'green');
+/* all six, where its siblings each carry five: a passphrase window has no reason
+ to prefer one phosphor, so it offers the whole set including the marquee cyan */
+screenChips('R57',$('card-R57').gw.el,['amber','green','red','blue','vfd','white'],'amber');
BOOST.forEach(no=>{const rd=document.getElementById('rd-'+no);
if(rd)rd.closest('.card').querySelector('.stagew').classList.add('boost');});
@@ -1585,25 +1671,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']]);