diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-17 18:34:43 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-17 18:34:43 -0500 |
| commit | 6d8f4d8bfae27e8fc6caf1cbc65cf0fdf7a85db8 (patch) | |
| tree | 007fd7c97871d0d23ae8db3afcf9951c1d48dcbe /docs/prototypes/panel-widget-gallery.html | |
| parent | 6bcd0da82c505a2ec22268adfced0004401b74a9 (diff) | |
| download | archsetup-6d8f4d8bfae27e8fc6caf1cbc65cf0fdf7a85db8.tar.gz archsetup-6d8f4d8bfae27e8fc6caf1cbc65cf0fdf7a85db8.zip | |
feat(gallery): colour-policy system, chip-toggle accents, widgets-per-row
Every widget's colour is either the consumer's to pick or locked for a reason, and I kept classifying cards wrong from memory during the colour pass. So the policy is a declared, checked property now. Six kinds. Accent and screen are free and take colour chips. Coded, emissive, relational and material are locked and must not. Each classified builder carries a POLICY record naming the kind, why this widget is bound that way, and what it may change and stay authentic (the range that actually existed, like the CRT phosphors that shipped). Eight are classified. The rest are the worklist.
The page shows all of it from that one source. A card recovers its builder from the arrow it was handed and reads GW.<builder>.POLICY, so the badge, the spec-sheet catalog and the index tally can't drift from the code. A probe holds it both ways: the recolour mechanism and the accent kind imply each other, so a locked card can't grow chips and an accent card can't forget to declare.
The chip toggle is the first consumer of a shared accent family (red, amber, green, white, vfd) from GW.accentStyles, so the next card that wants those five gets them from one definition rather than a copy.
Two smaller changes ride along because they touch the same header. The size toggle becomes WIDGETS/ROW, an explicit 1-4 column count instead of S/M/L zoom, and the policy panel sits level-right of the index.
Diffstat (limited to 'docs/prototypes/panel-widget-gallery.html')
| -rw-r--r-- | docs/prototypes/panel-widget-gallery.html | 139 |
1 files changed, 114 insertions, 25 deletions
diff --git a/docs/prototypes/panel-widget-gallery.html b/docs/prototypes/panel-widget-gallery.html index 25b10c6..95b3719 100644 --- a/docs/prototypes/panel-widget-gallery.html +++ b/docs/prototypes/panel-widget-gallery.html @@ -56,7 +56,10 @@ h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} .szbar .lab{color:var(--steel);font-size:.68rem;letter-spacing:.22em;text-transform:uppercase;margin-right:4px} /* masthead row: description left, index panel right */ -.mastrow{display:flex;gap:28px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap} +.mastrow{display:flex;gap:20px;align-items:flex-start;justify-content:flex-end;flex-wrap:wrap} +.mastintro{flex:1 1 340px} +#polpanel{max-width:250px} +#polpanel .pnote{color:var(--dim);font-size:.72rem;line-height:1.5;margin:2px 0 8px} .toc{position:relative;flex:0 0 auto;min-width:220px;padding:14px 20px 13px;border-radius:12px; background:linear-gradient(180deg,var(--raise),var(--panel));border:1px solid #262320; box-shadow:inset 0 1px 0 rgba(255,255,255,.05),0 6px 14px rgba(0,0,0,.4)} @@ -69,16 +72,25 @@ h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} .toc .scr{position:absolute;width:5px;height:5px;border-radius:50%; background:radial-gradient(circle at 35% 30%,#524d43,#161310);box-shadow:0 .5px 0 rgba(255,255,255,.08)} html{scroll-behavior:smooth} -/* size toggle scales the widget stage only; card text stays at its own (slightly raised) size */ -body[data-size="2"] .stagew{zoom:1.7} -body[data-size="3"] .stagew{zoom:2.4} +/* WIDGETS/ROW sets an explicit column count (1-4); the stage zoom scales with it + so a widget fills its column — fewer per row, bigger widgets. The card text + stays at its own (slightly raised) size, untouched by the zoom. */ +body[data-cols="1"] .grid{grid-template-columns:1fr} +body[data-cols="2"] .grid{grid-template-columns:repeat(2,1fr)} +body[data-cols="3"] .grid{grid-template-columns:repeat(3,1fr)} +body[data-cols="4"] .grid{grid-template-columns:repeat(4,1fr)} +body[data-cols="1"] .stagew{zoom:2.8} +body[data-cols="2"] .stagew{zoom:2.1} +body[data-cols="3"] .stagew{zoom:1.55} +body[data-cols="4"] .stagew{zoom:1.15} /* intrinsically tiny widgets get an extra bump so they do not float in an empty stage */ .stagew.boost{zoom:1.5} -body[data-size="2"] .stagew.boost{zoom:2.55} -body[data-size="3"] .stagew.boost{zoom:3.6} -body[data-size="2"] .grid{grid-template-columns:repeat(auto-fill,minmax(380px,1fr))} -body[data-size="3"] .grid{grid-template-columns:repeat(auto-fill,minmax(520px,1fr))} -body[data-size="2"] .wrap,body[data-size="3"] .wrap{max-width:none} +body[data-cols="1"] .stagew.boost{zoom:4.2} +body[data-cols="2"] .stagew.boost{zoom:3.15} +body[data-cols="3"] .stagew.boost{zoom:2.3} +body[data-cols="4"] .stagew.boost{zoom:1.7} +/* a single column shouldn't span the whole monitor; 2-4 keep the standard 1320 wrap */ +body[data-cols="1"] .wrap{max-width:960px} .secnote{color:var(--dim);font-size:.74rem;margin-top:.5rem;max-width:86ch} .palgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(176px,1fr));gap:10px;margin-top:1rem} @@ -103,7 +115,15 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} .wname{color:var(--cream);font-size:.95rem;font-weight:700;display:flex;align-items:center;gap:8px} .wname .no{color:var(--panel);background:var(--gold);border-radius:4px;font-size:.7rem;padding:0 5px;font-weight:400} /* validation lamp: off = not done, amber = in progress, green = done; click cycles, state persists */ -.vlamp{margin-left:auto;flex:0 0 auto;width:10px;height:10px;border-radius:50%;cursor:pointer; +/* colour-policy badge: green = free (consumer picks), amber = locked (a stated + reason it can't change), dim = not yet classified. The whole point is scanning + the page and seeing at a glance what the colour pass has and hasn't reached. */ +.cpol{margin-left:auto;flex:0 0 auto;font-size:.56rem;letter-spacing:.06em;text-transform:uppercase; + padding:1px 5px;border-radius:3px;border:1px solid var(--wash);color:var(--steel);cursor:default;font-weight:400} +.cpol[data-free="true"]{color:var(--pass);border-color:rgba(116,147,47,.5)} +.cpol[data-free="false"]{color:var(--gold-hi);border-color:rgba(255,190,84,.4)} +.cpol[data-pol="none"]{opacity:.45} +.vlamp{flex:0 0 auto;width:10px;height:10px;border-radius:50%;cursor:pointer; background:#221f1b;border:1px solid #33302b} .vlamp[data-v="amber"],.vtally .vdot[data-v="amber"],#vaudit .vdot[data-v="amber"]{background:var(--gold);border-color:#7d5c16;box-shadow:0 0 6px 1px rgba(var(--glow-lo),.6)} .vlamp[data-v="green"],.vtally .vdot[data-v="green"],#vaudit .vdot[data-v="green"]{background:var(--pass);border-color:#4a5c22;box-shadow:0 0 6px 1px rgba(116,147,47,.6)} @@ -171,13 +191,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} @media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}} </style> </head> -<body data-size="2"> +<body data-cols="3"> <div class="wrap"> <header class="masthead"> <div class="eyebrow">archsetup · dupre panel family · live kit</div> <h1>Widget gallery — the instrument-console kit, fully driveable</h1> <div class="mastrow"> - <div> + <div class="mastintro"> <p>Every control + display idiom in the dupre faceplate language, merged into one live console — the base kit, the candidate batch, and the growing reference batch (R: modeled on period hardware), all rendering from the same tokens the net / bt / sound panels use. @@ -185,10 +205,11 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} readouts</b> show state or a number. Every widget is operable: drag the faders, knobs and gauges; click the switches, keys and lamps. Each card carries a live readout that tracks what you do. The trace displays run a live signal; the pointer types drive from your drag.</p> - <div class="szbar"><span class="lab">widget size</span> - <button class="key" data-sz="1">S</button> - <button class="key on" data-sz="2">M</button> - <button class="key" data-sz="3">L</button> + <div class="szbar"><span class="lab">widgets/row</span> + <button class="key" data-cols="1">1</button> + <button class="key" data-cols="2">2</button> + <button class="key on" data-cols="3">3</button> + <button class="key" data-cols="4">4</button> </div> </div> <nav class="toc"> @@ -202,6 +223,13 @@ h2::after{content:"";height:1px;background:var(--wash);flex:1} <div class="tt" style="margin-top:11px">Validation</div> <div class="vtally" id="vtally"></div> </nav> + <nav class="toc" id="polpanel"> + <span class="scr" style="top:6px;left:6px"></span><span class="scr" style="top:6px;right:6px"></span> + <span class="scr" style="bottom:6px;left:6px"></span><span class="scr" style="bottom:6px;right:6px"></span> + <div class="tt">Policy</div> + <div class="pnote">What each widget's colour is bound by, and what it may change and stay true. Click a row to walk its cards; open a card's spec sheet for the reason and the authentic range.</div> + <div class="vtally ptally" id="ptally"></div> + </nav> </div> </header> @@ -226,14 +254,14 @@ const $ = id => document.getElementById(id); const {svgEl,polar,dragX,dragY,dragDelta,seg7,buildBars,VUDB,vuDb,SCREEN_FAMS}=GW; const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches; document.querySelectorAll('.szbar .key').forEach(b=>b.addEventListener('click',()=>{ - document.body.dataset.size=b.dataset.sz; - localStorage.setItem('gv-size',b.dataset.sz); + document.body.dataset.cols=b.dataset.cols; + localStorage.setItem('gv-cols',b.dataset.cols); document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k===b)); })); -(function(){const s=localStorage.getItem('gv-size'); - if(!s||!/^[123]$/.test(s))return; - document.body.dataset.size=s; - document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k.dataset.sz===s));})(); +(function(){const s=localStorage.getItem('gv-cols'); + if(!s||!/^[1234]$/.test(s))return; + document.body.dataset.cols=s; + document.querySelectorAll('.szbar .key').forEach(k=>k.classList.toggle('on',k.dataset.cols===s));})(); /* boost the intrinsically tiny widgets (audit: content under 10% of the stage) */ const BOOST=['01','07','08','12','13','N14','18']; @@ -684,6 +712,15 @@ 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.'}, +'R58':{input:'Click a character to move the stylus, then pull the lever to print it. Or click the plate and type: keys move the stylus, Enter is the lever. Selecting never prints.', + solves:'Free text where the operator must SEE the whole character set and commit deliberately — the plate is the documentation, and nothing lands by accident.', + use:'Specialty. Shines where a stray keypress must not enter anything, and where the available characters are worth showing rather than remembering.', + limits:'Two acts per character, so it is the slowest text control in the kit by design. No space cell (the real machine has a separate space key); no 1 or 0 (type them with lowercase l and capital O).', + origin:'AEG Mignon Model 4 (1924), the best of the index typewriters. Plate transcribed from the reference photo; layout and zones are declared tables.', + difficulty:'Obvious to read, slow to use. The two-handed grammar is the point, not an obstacle.', + prefer:'Deliberate commitment matters more than speed, or the character set itself needs to be on display. R57 is the fast alternative and R16 the digits-only one.', + period:'1903-1934 (index typewriters); the idiom predates the keyboard becoming standard.', + ref:'../../working/retro-stereo-widgets/references/2026-07-16-mignon-aeg-detail.jpg'}, '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.', @@ -1158,10 +1195,31 @@ addEventListener('keydown',e=>{if(e.key==='Escape')auditStop();}); /* card(host, no, name, htmlOrBuild, note) — the declarative card record. htmlOrBuild: a legacy stage-HTML string, or a builder function (stage, rd) => handle that instantiates a GW.* widget into the stage; rd(txt) writes the card readout. */ +/* Recover a card's builder from the arrow it was handed — the first GW.<name>( + in the source — and read its POLICY record off it. Single source: the record + lives on GW.<builder>.POLICY in widgets.js and the card only displays it, so the + page can't drift from the code. Unclassified cards read '—', which is the point: + the gallery becomes the colour pass's visible worklist. */ +function cardPolicy(html){ + if(typeof html!=='function') return null; + /* Assumes the FIRST GW.<name>( in the arrow is the builder — true because every + card is (st,rd)=>GW.builder(st,{...}) and helpers are destructured, called + bare. An arrow that referenced a second GW.<name>( before its builder would + mis-derive; none do. A wrapped or renamed builder reads '—' rather than + wrong, which is safe. */ + const m=html.toString().match(/GW\.(\w+)\s*\(/); + return (m && GW[m[1]] && GW[m[1]].POLICY) || null; +} function card(host, no, name, html, note){ const isBuild=typeof html==='function'; + const P=cardPolicy(html), kind=P&&P.kind; + const free=kind&&GW.POLICIES[kind]&&GW.POLICIES[kind].free; + const polTip=kind?(GW.POLICIES[kind].gist||''):'policy not yet classified'; const c=document.createElement('div'); c.className='card'; c.id='card-'+no; - c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}<span class="vlamp" data-v="off"></span></div>`+ + c.dataset.cpol=kind||'none'; + c.innerHTML=`<div class="wname"><span class="no">${no}</span>${name}`+ + `<span class="cpol" data-pol="${kind||'none'}" data-free="${kind?free:''}" title="${polTip.replace(/"/g,'"')}">${kind||'—'}</span>`+ + `<span class="vlamp" data-v="off"></span></div>`+ `<div class="stagew">${isBuild?'':html}</div><div class="wrd" id="rd-${no}">—</div>`+ `<div class="opts"></div><div class="wnote">${note}</div>`; const lamp=c.querySelector('.vlamp'), VKEY='gv-'+no, VSTATES=['off','amber','green']; @@ -1173,6 +1231,13 @@ function card(host, no, name, html, note){ const inf=(typeof INFO!=='undefined')&&INFO[no]; if(inf){const d=document.createElement('details'); d.className='winfo'; let rows=''; + if(P){ + rows+=`<div class="ik">policy</div><div class="iv"><b>${kind}</b> — ${GW.POLICIES[kind].gist}</div>`; + rows+=`<div class="ik">why</div><div class="iv">${P.why}</div>`; + rows+=`<div class="ik">authentic</div><div class="iv">${P.authentic}</div>`; + } else { + rows+=`<div class="ik">policy</div><div class="iv">unclassified — colour pass pending</div>`; + } for(const [k,lbl] of INFO_FIELDS) if(inf[k]) rows+=`<div class="ik">${lbl}</div><div class="iv">${inf[k]}</div>`; if(inf.ref) rows+=`<div class="ik">reference</div><div class="iv"><a href="${inf.ref}" target="_blank">photo</a></div>`; d.innerHTML=`<summary>spec sheet</summary><div class="igrid">${rows}</div>`; @@ -1182,6 +1247,28 @@ function card(host, no, name, html, note){ return c; } function setRd(no,txt){const e=$('rd-'+no);if(e)e.textContent=txt;} +/* Colour-policy tally: free / locked / unclassified across all cards, so the + index shows how far the colour pass has reached the way the validation tally + shows the walk. Free = accent+screen; locked = the four protected policies; + the rest await classification. Clicking a row jumps through its cards. */ +const FREEP=['accent','screen'], LOCKP=['coded','emissive','relational','material']; +const PJUMP={free:0,locked:0,none:0}; +function updatePolTally(){ + const el=$('ptally'); if(!el)return; + const cards=[...document.querySelectorAll('.card')]; + const grp=p=>FREEP.includes(p)?'free':LOCKP.includes(p)?'locked':'none'; + const n={free:0,locked:0,none:0}; + cards.forEach(c=>{n[grp(c.dataset.cpol)]++;}); + el.innerHTML= + `<div class="vrow" data-g="free"><span class="vdot" data-v="green"></span>free<span class="vn">${n.free}</span></div>`+ + `<div class="vrow" data-g="locked"><span class="vdot" data-v="amber"></span>locked<span class="vn">${n.locked}</span></div>`+ + `<div class="vrow" data-g="none"><span class="vdot"></span>unclassified<span class="vn">${n.none}</span></div>`+ + `<div class="vrow vtot">total<span class="vn">${cards.length}</span></div>`; + el.querySelectorAll('.vrow[data-g]').forEach(row=>row.addEventListener('click',()=>{ + const g=row.dataset.g, hits=cards.filter(c=>grp(c.dataset.cpol)===g); + if(!hits.length)return; const i=PJUMP[g]++%hits.length; + location.hash=''; location.hash=hits[i].id;})); +} /* drag helpers, seg7, buildBars live in widgets.js (GW) */ /* ============ CONTROLS ============ */ @@ -1210,7 +1297,7 @@ card(C,'06','Segmented selector', '<b>pick one of a few.</b> Timer type, layout mode. Click a segment; readout names the choice. The lit segment ships in amber, green, or red — the accent chips below switch it.'); card(C,'07','Chip toggle', (st,rd)=>GW.chipToggle(st,{on:true,onChange:(v,t)=>rd(t)}), - '<b>inline binary.</b> A soft toggle inside a line of text. Click to flip; gold when on.'); + '<b>inline binary.</b> A soft toggle inside a line of text. Click to flip. The lit colour is the consumer\'s to pick from the accent family, because "on" is good news in one panel and a fault in the next — amber by default.'); card(C,'08','Arm-to-fire', (st,rd)=>GW.armButton(st,{onChange:(v,t)=>rd(t)}), '<b>two-stage confirm.</b> Destructive actions. First click arms (red), second fires. Readout shows the stage.'); @@ -1355,7 +1442,7 @@ card(C,'R57','ABC entry keypad', 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.'); + '<b>type without a keyboard.</b> One hand walks the stylus over a printed plate, 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. The plate is the AEG Mignon Model 4\'s own (1924), transcribed from the reference: both full cases so there is no shift key, the accents and fractions a keyboard skips, and two economies worth noticing. There is <b>no 1 and no 0</b> — you type them with lowercase l and capital O. And the inversion is positional, not by case: the capitals block is dark discs on a light panel and the lowercase block is its photographic negative, but J and j sit out on the halftone ring, un-inverted, with the punctuation.'); /* ============ METERS & GAUGES ============ */ const M=$('meters'); @@ -1724,11 +1811,13 @@ styleChips('01',GW.slideToggle.STYLES, 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('07',GW.chipToggle.STYLES,[['accent','accent','amber']]); styleChips('25',GW.slideRule.STYLES,[['face','skin','warm']]); /* final tally pass: setV fires per card during build, but each card is still detached at that moment, so the running counts lag by one — recount now */ updateVTally(); +updatePolTally(); </script> </body> </html> |
