aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-03-net-panel-rescan-prototype.html
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-04 16:46:48 -0400
committerCraig Jennings <c@cjennings.net>2026-07-04 16:46:48 -0400
commit518ffd7578dbc74689b5303a35f402bfe081aa91 (patch)
treee22784c9b34334b69f6f6074c010ce289c129134 /docs/prototypes/2026-07-03-net-panel-rescan-prototype.html
parent9945ad041fca214c2f6c761ce9fd1ccf1759a8ac (diff)
downloadarchsetup-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.html251
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>