← back to Tools Dw Hub
tools-dw-hub: href-drill — card category is a filter-link + URL-addressable state
1b68235060f534cdd872ed084a3a1dc9daac4ab6 · 2026-08-01 01:57:09 -0700 · Steve Abrams
Each card's category is now an <a href="?category=…"> to its filtered view (href-drill
HARD rule); left-panel facets (category/status/type) + search are URL-addressable. Click
filters in place + reflects into the URL, cmd/ctrl-click opens the shareable href,
back-button + deep-link restore via popstate/readURL.
Verified (Playwright + basic auth, live :9950): click category 'Room Settings' -> ?category
+ grid 29->3 all-matching; deep-link restores; back-button->29; canary FLAT->DONE; 0 err.
href-drill backlog grid 7/92. TK-10089.
Files touched
Diff
commit 1b68235060f534cdd872ed084a3a1dc9daac4ab6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 1 01:57:09 2026 -0700
tools-dw-hub: href-drill — card category is a filter-link + URL-addressable state
Each card's category is now an <a href="?category=…"> to its filtered view (href-drill
HARD rule); left-panel facets (category/status/type) + search are URL-addressable. Click
filters in place + reflects into the URL, cmd/ctrl-click opens the shareable href,
back-button + deep-link restore via popstate/readURL.
Verified (Playwright + basic auth, live :9950): click category 'Room Settings' -> ?category
+ grid 29->3 all-matching; deep-link restores; back-button->29; canary FLAT->DONE; 0 err.
href-drill backlog grid 7/92. TK-10089.
---
public/index.html | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/public/index.html b/public/index.html
index e680711..1b142dc 100644
--- a/public/index.html
+++ b/public/index.html
@@ -127,6 +127,12 @@ const LS={get:(k,d)=>{try{return JSON.parse(localStorage.getItem('dwtools_'+k))?
// facet fields — each a collapsed table in the left panel
const FIELDS=[['category','Category'],['status','Status'],['type','Type']];
const FIL={category:'',status:'',type:''};
+// ── href-drill: FIL/search state is URL-addressable (shareable + deep-link + back-button) ──
+const escU=s=>encodeURIComponent(String(s==null?'':s));
+function qstr(over){ const f=Object.assign({},FIL,over||{}); const u=[]; for(const [k] of FIELDS) if(f[k]) u.push(k+'='+escU(f[k])); const q=($('#q')&&$('#q').value.trim())||''; if(q)u.push('q='+escU(q)); return u.length?('?'+u.join('&')):location.pathname; }
+function drill(field,val,label){ if(val==null||val===''||val==='—')return esc(label!=null?label:val); return `<a class="drill" data-filter="${field}" data-v="${esc(val)}" href="${esc(qstr({[field]:String(val)}))}" title="Filter to ${esc(val)}">${esc(label!=null?label:val)}</a>`; }
+function readURL(){ const u=new URLSearchParams(location.search); for(const [k] of FIELDS) FIL[k]=u.get(k)||''; if($('#q'))$('#q').value=u.get('q')||''; }
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
const typeOf=t=>t.missingSource?'Missing source':t.cli?'CLI':t.static?'Static':t.onDemand?'On-demand':'Web app';
const statusOf=t=>{const s=STATUS[t.slug];return s==='up'?'Up':s==='down'?'Down':'Idle';};
const valOf=(t,k)=>k==='status'?statusOf(t):k==='type'?typeOf(t):(t[k]||'—');
@@ -186,7 +192,7 @@ function render(){
return `<div class="card${launchable?' launchable':''}" ${launchable?`data-href="${url}"`:''}>
<div class="ct"><span class="name">${esc(t.name)}</span><span class="dot ${cls}" title="${st}"></span></div>
<div class="desc">${esc(t.desc)}</div>
- <div class="meta"><span class="cat">${esc(t.category)}</span><span title="${t.created}">🕓 ${fmtDate(t.created)}</span>${t.port?`<span>:${t.port}</span>`:''}${badges}</div>
+ <div class="meta"><span class="cat">${drill('category',t.category)}</span><span title="${t.created}">🕓 ${fmtDate(t.created)}</span>${t.port?`<span>:${t.port}</span>`:''}${badges}</div>
<div class="actions">${go}${sb}<button class="act copy" data-cmd="${esc(t.start||'')}">Copy start ⌘</button></div>
</div>`;
}).join('');
@@ -218,8 +224,12 @@ function toast(m){const el=$('#toast');el.textContent=m;el.classList.add('on');c
// facet click (delegated)
$('#facetbox').addEventListener('click',e=>{const row=e.target.closest('.frow');if(!row)return;
- const{k,v}=row.dataset; FIL[k]=FIL[k]===v?'':v; renderSide(); render();});
-$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';renderSide();render();};
+ const{k,v}=row.dataset; FIL[k]=FIL[k]===v?'':v; writeURL(true); renderSide(); render();});
+$('#clearf').onclick=()=>{FIELDS.forEach(([k])=>FIL[k]='');$('#q').value='';writeURL(true);renderSide();render();};
+// a card's category is an href-drill atom → filter in place + reflect into the URL (cmd/ctrl-click opens the shareable href)
+document.addEventListener('click',e=>{ const d=e.target.closest('a.drill[data-filter]'); if(!d)return; if(e.metaKey||e.ctrlKey||e.shiftKey||e.button===1)return; e.preventDefault(); e.stopPropagation();
+ const k=d.dataset.filter,v=d.dataset.v; FIL[k]=(FIL[k]===v?'':v); writeURL(true); renderSide(); render(); });
+addEventListener('popstate',()=>{ readURL(); renderSide(); render(); }); // back/forward restores filter state
async function loadStatus(){try{STATUS=await(await fetch(O+'/api/status')).json();renderSide();render();}catch{}}
@@ -230,13 +240,13 @@ async function init(){
$('#sort').value=LS.get('sort','newest');
const dens=LS.get('density',5); $('#density').value=dens; document.documentElement.style.setProperty('--cols',dens); setFs(dens);
if(LS.get('imgonly',false)){$('#imgonly').checked=true;document.body.classList.add('imgonly');}
- $('#q').oninput=()=>{renderSide();render();};
+ $('#q').oninput=()=>{writeURL(false);renderSide();render();};
$('#sort').onchange=()=>{LS.set('sort',$('#sort').value);render();};
$('#density').oninput=()=>{const v=+$('#density').value;document.documentElement.style.setProperty('--cols',v);setFs(v);LS.set('density',v);};
$('#imgonly').onchange=()=>{document.body.classList.toggle('imgonly',$('#imgonly').checked);LS.set('imgonly',$('#imgonly').checked);};
$('#burger').onclick=()=>document.body.classList.add('filters');
$('#fclose').onclick=()=>document.body.classList.remove('filters');
- renderSide(); render(); await loadStatus(); setInterval(loadStatus,20000);
+ readURL(); renderSide(); render(); await loadStatus(); setInterval(loadStatus,20000); // land in URL's filter state
$('#foot').textContent='Live status refreshes every 20s. Green = running (marker-verified over tailnet) — click a card or Launch to open it. Red = stopped; use Start.';
}
// density → type scale (fewer/larger cards → bigger type)
← 3e7e390 security: remove DW2024! hardcoded creds from pm2 WRAP — now
·
back to Tools Dw Hub
·
nav-agent: universal grid-controls drop-in on internal dashb 8c962db →