diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-04 16:46:48 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-04 16:46:48 -0400 |
| commit | 518ffd7578dbc74689b5303a35f402bfe081aa91 (patch) | |
| tree | e22784c9b34334b69f6f6074c010ce289c129134 /docs/prototypes/2026-07-03-net-panel-rescan-prototype.html | |
| parent | 9945ad041fca214c2f6c761ce9fd1ccf1759a8ac (diff) | |
| download | archsetup-518ffd7578dbc74689b5303a35f402bfe081aa91.tar.gz archsetup-518ffd7578dbc74689b5303a35f402bfe081aa91.zip | |
docs: gather panel design prototypes into docs/prototypes/
I gathered all five self-contained HTML/CSS design prototypes into one home: the instrument-console pair (moved from assets/), plus the net-panel rescan, sound panel, widget gallery, and waybar redesign (moved out of working/). Added a README index and updated every inbound link: build summary, the instrument-console and audio specs, and todo.org.
Also fixed a broken link the earlier sort left in the build summary. It still pointed at the instrument-console spec's old docs/design/ path after the move to docs/specs/.
Diffstat (limited to 'docs/prototypes/2026-07-03-net-panel-rescan-prototype.html')
| -rw-r--r-- | docs/prototypes/2026-07-03-net-panel-rescan-prototype.html | 251 |
1 files changed, 251 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html b/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html new file mode 100644 index 0000000..3329cdb --- /dev/null +++ b/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html @@ -0,0 +1,251 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>Net panel — rescan affordance</title> +<style> +:root{ + --ground:#151311; --panel:#100f0f; --well:#0a0c0d; --raise:#1a1917; + --gold:#dab53d; --gold-hi:#ffd75f; --silver:#bfc4d0; --cream:#f3e7c5; + --steel:#969385; --dim:#7c838a; --slate:#424f5e; --slate-hi:#54677d; + --wash:#2c2f32; --pass:#74932f; --fail:#cb6b4d; + --mono:"BerkeleyMono Nerd Font","Berkeley Mono",monospace; +} +*{box-sizing:border-box;margin:0;padding:0} +html{background:var(--ground)} +body{font-family:var(--mono);color:var(--silver);padding:2.4rem 2rem 4rem;line-height:1.45; + background:radial-gradient(1200px 600px at 70% -10%,#1c1915 0%,transparent 60%),var(--ground)} +.masthead{max-width:1200px;margin:0 auto 1.8rem} +.eyebrow{color:var(--steel);font-size:.72rem;letter-spacing:.28em;text-transform:uppercase} +h1{color:var(--gold);font-size:1.5rem;margin:.35rem 0 .4rem} +.masthead p{color:var(--dim);font-size:.86rem;max-width:82ch} +.masthead p b{color:var(--silver)} + +.stage{display:flex;gap:2.2rem;flex-wrap:wrap;max-width:1200px;margin:0 auto;align-items:flex-start} +.slot{width:400px} +.slot-label{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:0 0 .55rem .2rem} +.panel{background:var(--panel);border:2px solid var(--gold);border-radius:16px;padding:17px 19px; + box-shadow:0 18px 50px rgba(0,0,0,.55);font-size:13.5px;width:380px} + +.lamp{width:9px;height:9px;border-radius:50%;background:var(--pass);flex:0 0 auto;box-shadow:0 0 6px 1px rgba(116,147,47,.55)} +.lamp.gold{background:var(--gold);box-shadow:0 0 6px 1px rgba(218,181,61,.55)} +.lamp.off{background:var(--wash);box-shadow:none} +.lamp.busy{background:var(--gold);animation:pulse .7s ease-in-out infinite} +@keyframes pulse{50%{opacity:.25}} + +.b-face{background:var(--raise);border-radius:12px;border:1px solid #262320;padding:11px 14px} +.b-id{display:flex;align-items:center;gap:9px} +.b-id .state-word{color:var(--gold);font-weight:700;font-size:15px;letter-spacing:.12em} +.b-id .unit{color:var(--steel);font-size:.68rem;letter-spacing:.3em;margin-left:auto} + +.chan{margin-top:12px} +.chan .line1{display:flex;align-items:baseline;gap:9px} +.chan .ssid{color:var(--cream);font-weight:700;font-size:14.5px} +.chan .line2{color:var(--dim);font-size:11.5px;margin-top:2px} +.ladder{display:inline-flex;gap:2px;align-items:flex-end;height:12px} +.ladder i{width:4px;background:var(--wash);border-radius:1px} +.ladder i:nth-child(1){height:4px}.ladder i:nth-child(2){height:7px} +.ladder i:nth-child(3){height:10px}.ladder i:nth-child(4){height:12px} +.ladder.l1 i:nth-child(-n+1){background:var(--gold)}.ladder.l2 i:nth-child(-n+2){background:var(--gold)} +.ladder.l3 i:nth-child(-n+3){background:var(--gold)}.ladder.l4 i{background:var(--gold)} + +/* engrave header with the rescan action */ +.engrave{color:var(--steel);font-size:.64rem;letter-spacing:.24em;text-transform:uppercase; + display:flex;align-items:center;gap:8px;margin:14px 0 6px} +.engrave::before{content:"";height:1px;background:var(--wash);width:10px;flex:0 0 auto} +.engrave .cnt{color:var(--dim);letter-spacing:.08em;text-transform:none;cursor:pointer; + border-bottom:1px dotted transparent} +.engrave .cnt:hover{color:var(--gold);border-bottom-color:var(--wash)} +.engrave .cnt.scanning{color:var(--gold);animation:breathe 1.1s ease-in-out infinite;cursor:default;border-bottom-color:transparent} +.engrave .spacer{flex:1;height:1px;background:var(--wash)} +/* compact rescan glyph — sits right after the count, spins while scanning */ +.engrave .ricon{color:var(--dim);cursor:pointer;font-size:.82rem;display:inline-flex;line-height:1} +.engrave .ricon:hover{color:var(--gold)} +.engrave .ricon.spin{color:var(--gold);cursor:default;animation:spin .9s linear infinite} +.engrave .act{color:var(--dim);letter-spacing:.06em;text-transform:none;font-size:.72rem;cursor:pointer} +.engrave .act:hover{color:var(--gold)} +@keyframes spin{to{transform:rotate(360deg)}} +@keyframes breathe{0%,100%{opacity:1}50%{opacity:.35}} + +/* the list; section-breathe busy style pulses the whole well */ +#networks{border-radius:8px;transition:background .3s} +#networks.breathe{animation:sectionbreathe 1.4s ease-in-out infinite} +@keyframes sectionbreathe{0%,100%{background:transparent}50%{background:rgba(218,181,61,.06)}} + +.lamp-row{display:flex;align-items:center;gap:9px;padding:5px 6px;border-radius:7px;font-size:12.5px;cursor:pointer} +.lamp-row:hover{background:var(--wash)} +.lamp-row .who{color:var(--silver);white-space:nowrap} +.lamp-row .who b{color:var(--cream)} +.lamp-row .what{margin-left:auto;color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.lamp-row.fresh{animation:fadein .5s ease} +@keyframes fadein{from{opacity:0;transform:translateY(-3px);background:rgba(218,181,61,.12)}to{opacity:1}} + +.toast{margin-top:10px;font-size:11px;color:var(--cream);background:var(--slate);border-radius:7px; + padding:5px 10px;opacity:0;transition:opacity .25s;min-height:1.4em} +.toast.show{opacity:1} + +.aside{flex:1 1 320px;min-width:290px} +.aside h3{color:var(--steel);font-size:.7rem;letter-spacing:.22em;text-transform:uppercase;margin:1.1rem 0 .5rem} +.aside h3:first-child{margin-top:.2rem} +.aside ul{list-style:none} +.aside li{font-size:.82rem;padding:.24rem 0 .24rem 1.1rem;position:relative} +.aside li::before{content:"·";color:var(--gold);position:absolute;left:.25rem} +.aside li b{color:var(--cream);font-weight:700} +.aside li em{color:var(--dim);font-style:normal} +.demo{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:1rem} +.demo .lbl{color:var(--steel);font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;margin-bottom:.5rem} +.seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden;margin-bottom:.7rem} +.seg button{flex:1;font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 6px;cursor:pointer} +.seg button:last-child{border-right:0} +.seg button.on{background:linear-gradient(180deg,#f0d879,var(--gold));color:var(--panel);font-weight:700} +.demo .go{font:inherit;font-size:.8rem;color:var(--silver);background:transparent;border:1px solid var(--gold); + border-radius:8px;padding:.45rem 1rem;cursor:pointer} +.demo .go:hover{background:rgba(218,181,61,.12)} +.rec{border:1px dashed var(--wash);border-radius:10px;padding:.85rem 1rem;margin-top:.9rem;font-size:.82rem} +.rec b{color:var(--gold)} +@media (prefers-reduced-motion:reduce){*{animation:none!important}} +</style> +</head> +<body> + +<header class="masthead"> + <div class="eyebrow">archsetup · dupre panel family · net·01</div> + <h1>Networks — the rescan affordance</h1> + <p>Where does a WiFi rescan live, and how does it show it's working? The count "· N in range" is + really a live status field — the natural home for "scanning…". My lean: an explicit <b>⟳ rescan</b> + action in the engrave line (discoverable, same slot as "+ hidden"), with a <b>flash-and-fade</b> + busy state — the glyph spins, the count breathes, new rows fade in as found. The count is also + click-to-rescan as a shortcut. Use the selector on the right to feel each busy treatment.</p> +</header> + +<div class="stage"> + <div class="slot"> + <div class="slot-label">net·01 — networks section</div> + <div class="panel"> + <div class="b-face"> + <div class="b-id"> + <span class="lamp"></span> + <span class="state-word">ONLINE</span> + <span class="unit">NET·01</span> + </div> + </div> + + <div class="engrave">channel</div> + <div class="chan"> + <div class="line1"><span class="ssid">@Hyatt_WiFi</span> + <span class="ladder l3"><i></i><i></i><i></i><i></i></span> + <span class="dim" style="font-size:11px">-59 dBm · 44 ms</span></div> + <div class="line2">172.20.2.108/20 · gw 172.20.0.1 · route wlp170s0</div> + </div> + + <div class="engrave">networks<span class="cnt" id="cnt" onclick="rescan()">· 5 in range</span> + <span class="ricon" id="rescan" onclick="rescan()" title="Rescan for networks">⟳</span> + <span class="spacer"></span> + <span class="act" onclick="toast('would open the hidden-SSID dialog')">+ hidden</span> + </div> + <div id="networks"></div> + + <div class="toast" id="toast"></div> + </div> + </div> + + <div class="aside"> + <div class="demo"> + <div class="lbl">busy feedback style</div> + <div class="seg" id="styleSeg"> + <button class="on" data-s="all">all (rec)</button> + <button data-s="spin">spinner</button> + <button data-s="count">count</button> + <button data-s="section">section</button> + </div> + <button class="go" onclick="rescan()">▶ run a rescan</button> + </div> + + <h3>The three busy signals</h3> + <ul> + <li><b>Spinner</b> — the ⟳ glyph rotates while scanning. The clearest "working" cue; universal.</li> + <li><b>Count breathe</b> — "· 5 in range" becomes "scanning…" and slow-pulses. Your idea: the status field animates in place, no extra chrome.</li> + <li><b>Section breathe</b> — the whole list gives a faint gold breath while the scan runs; found rows fade in gold. Ambient, ties the animation to what's changing.</li> + <li><b>All (recommended)</b> — spinner + count + fade-in together. The section breathe is optional; it can read as busy on a small panel, so it's off in "all" by default and its own option to try.</li> + </ul> + + <h3>Why an explicit action, not only the count</h3> + <div class="rec"> + Overloading the count as the sole trigger is elegant but a first look doesn't know it's clickable. + An explicit <b>⟳ rescan</b> in the engrave line is discoverable, sits in the same slot as "+ hidden" + (consistent), and doesn't cost a heavy console key. Keeping the count clickable too gives power users + the shortcut without hiding the affordance. A dedicated <b>RESCAN console key</b> (next to DOCTOR / + SPEED TEST) is the third option — heavier, and rescan is a list action, not a diagnostic, so it fits + the engrave line better. + </div> + </div> +</div> + +<script> +const $=id=>document.getElementById(id); +let busy=false, style='all'; +let NETS=[ + {ssid:'@Hyatt_WiFi', sig:3, sec:'WPA2', stored:true, active:true}, + {ssid:'Hyatt_Meeting', sig:3, sec:'WPA2', stored:false, active:false}, + {ssid:'DIRECT-roku-882',sig:2, sec:'WPA2', stored:false, active:false}, + {ssid:'xfinitywifi', sig:1, sec:null, stored:false, active:false}, + {ssid:'HomeNet', sig:0, sec:'WPA2', stored:true, active:false, oor:true}, +]; +const NEWFOUND=[ + {ssid:'Hyatt_Guest', sig:2, sec:null, stored:false, active:false}, + {ssid:'Marriott_CONF', sig:1, sec:'WPA2', stored:false, active:false}, +]; +const pctFor=[null,'22%','44%','61%','78%']; + +$('styleSeg').addEventListener('click',e=>{const b=e.target.closest('button');if(!b)return; + style=b.dataset.s;[...$('styleSeg').children].forEach(x=>x.classList.toggle('on',x===b));}); + +function rowEl(n,fresh){ + const r=document.createElement('div'); r.className='lamp-row'+(fresh?' fresh':''); + const lamp=n.active?'lamp':(n.oor?'lamp off':'lamp gold'); + const what=n.active?'active · '+(n.sec||'open') + :n.oor?'stored · out of range' + :(n.stored?'stored · ':'')+(n.sec||'open')+' · '+pctFor[n.sig]; + r.innerHTML=`<span class="${lamp}"></span><span class="who">${n.active?'<b>'+n.ssid+'</b>':n.ssid}</span>`+ + (!n.active&&!n.oor?`<span class="ladder l${n.sig}" style="margin-left:6px"><i></i><i></i><i></i><i></i></span>`:'')+ + `<span class="what">${what}</span>`; + r.onclick=()=>{ if(busy)return; toast(n.active?'already on '+n.ssid:'would join '+n.ssid); }; + return r; +} +function render(freshSet){ + const host=$('networks'); host.innerHTML=''; + const inRange=NETS.filter(n=>!n.oor).sort((a,b)=>(b.active-a.active)||(b.sig-a.sig)); + const oor=NETS.filter(n=>n.oor); + [...inRange,...oor].forEach(n=>host.appendChild(rowEl(n,freshSet&&freshSet.has(n.ssid)))); + if(!busy) $('cnt').textContent='· '+inRange.length+' in range'; +} +function rescan(){ + if(busy) return; busy=true; + const useSpin = style==='all'||style==='spin'; + const useCount= style==='all'||style==='count'; + const useSec = style==='section'; + if(useSpin) $('rescan').classList.add('spin'); + if(useCount){ $('cnt').classList.add('scanning'); $('cnt').textContent='scanning…'; } + if(useSec) $('networks').classList.add('breathe'); + toast('scanning for networks…'); + // networks trickle in as "found" + const fresh=new Set(); + setTimeout(()=>{ NETS.splice(1,0,NEWFOUND[0]); fresh.add(NEWFOUND[0].ssid); render(fresh); },900); + setTimeout(()=>{ NETS.splice(3,0,NEWFOUND[1]); fresh.add(NEWFOUND[1].ssid); render(fresh); },1700); + setTimeout(()=>{ + busy=false; + $('rescan').classList.remove('spin'); + $('cnt').classList.remove('scanning'); + $('networks').classList.remove('breathe'); + const n=NETS.filter(x=>!x.oor).length; + $('cnt').textContent='· '+n+' in range'; + toast('scan complete — '+n+' networks in range'); + // reset for the next run so the demo is repeatable + NETS=NETS.filter(x=>x.ssid!=='Hyatt_Guest'&&x.ssid!=='Marriott_CONF'); + },2600); +} +render(); +</script> +</body> +</html> |
