[object Object]

← back to Rejected Prompts Viewer

wire drill.js: category badges and project names are now filter hrefs

4f6cb19f4afe497096f73e4b397d245e1238ac7d · 2026-08-24 04:14:17 -0700 · Steve

- .cat badge on every card is an <a> using catHref() — shareable deep-link,
  plain-click toggles the category filter in-place (cmd/ctrl = new tab)
- project name in metaRow is an <a> using projectHref() — plain-click sets
  the search field to that project name and re-renders
- add project field to filtered() search scope so project-name searches work
- add delegated click handlers for [data-drill-cat] and [data-drill-proj]
- copy drill.js from fabricut-internal as the canonical library (TK-10795)

Files touched

Diff

commit 4f6cb19f4afe497096f73e4b397d245e1238ac7d
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Aug 24 04:14:17 2026 -0700

    wire drill.js: category badges and project names are now filter hrefs
    
    - .cat badge on every card is an <a> using catHref() — shareable deep-link,
      plain-click toggles the category filter in-place (cmd/ctrl = new tab)
    - project name in metaRow is an <a> using projectHref() — plain-click sets
      the search field to that project name and re-renders
    - add project field to filtered() search scope so project-name searches work
    - add delegated click handlers for [data-drill-cat] and [data-drill-proj]
    - copy drill.js from fabricut-internal as the canonical library (TK-10795)
---
 public/drill.js   | 36 +++++++++++++++++++++++++++++++
 public/index.html | 64 +++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 93 insertions(+), 7 deletions(-)

diff --git a/public/drill.js b/public/drill.js
new file mode 100644
index 0000000..e63cdd3
--- /dev/null
+++ b/public/drill.js
@@ -0,0 +1,36 @@
+// ── href-to-deeper-data primitives — SINGLE SOURCE OF TRUTH (HARD rule, 2026-07-31) ──
+// Every displayed data point must be an href to its deeper (filtered) data. These are the
+// reusable atoms; a viewer's index.html loads this file, then defines FIELDS, FIL, $, esc,
+// and applyFilters (which these reference at call time via the shared classic-script global
+// scope). Canonical copy: ~/Projects/_shared/web/drill.js → each viewer's public/drill.js.
+// Memory: all-data-points-href-to-deeper-data. Reference viewers: astek-landing, schumacher-internal.
+
+// Build the URL for a given filter set — any count/atom becomes a real, shareable link.
+function qstr(fil, q, pat) {
+  const u = new URLSearchParams();
+  for (const [k] of FIELDS) { if (fil[k]) u.set(k, fil[k]); }
+  if (q) u.set('q', q); if (pat) u.set('pat', pat);
+  const s = u.toString(); return s ? ('?' + s) : location.pathname;
+}
+// Read the URL's filter state back into FIL + the search inputs (deep-link / back-button).
+function readURL() {
+  const u = new URLSearchParams(location.search);
+  for (const [k] of FIELDS) FIL[k] = u.get(k) || '';
+  $('#q').value = u.get('q') || ''; $('#pattern').value = u.get('pat') || '';
+}
+// Reflect the current filter state into the URL (push = new history entry, else replace).
+function writeURL(push) {
+  const url = qstr(FIL, $('#q').value.trim(), $('#pattern').value.trim());
+  history[push ? 'pushState' : 'replaceState']({}, '', url);
+}
+// drill(field,value,label,cls) → an <a> whose href IS the filtered view for that value.
+// cmd/ctrl/middle-click opens it in a new tab; plain click filters in place (delegated listener).
+function drill(field, val, label, cls) {
+  if (val == null || val === '') return '';
+  const href = qstr({ ...FIL, [field]: String(val) }, $('#q').value.trim(), $('#pattern').value.trim());
+  return `<a class="drill ${cls || ''}" href="${href}" data-filter="${field}" data-v="${esc(val)}" title="Filter to ${esc(val)}">${label != null ? esc(label) : esc(val)}</a>`;
+}
+// Atom drill target: SET the filter to this value (drill in), vs pickF which TOGGLES.
+function setF(k, v) { if (FIL[k] === String(v)) return; FIL[k] = String(v); applyFilters('push'); }
+
+window.qstr = qstr; window.readURL = readURL; window.writeURL = writeURL; window.drill = drill; window.setF = setF;
diff --git a/public/index.html b/public/index.html
index d0e8c34..8b82f9d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -32,9 +32,13 @@
   .card.borderline { border-style:dashed; opacity:.72; }
   .badges { display:flex; gap:6px; align-items:center; }
   .cat { font-size:11px; font-weight:700; letter-spacing:.4px; text-transform:uppercase; padding:3px 9px; border-radius:6px; }
+  a.cat { text-decoration:none; }
+  a.cat:hover { filter:brightness(1.2); }
   .cat.copyright{background:#3a2a12;color:#f6c667;} .cat.trademark{background:#132f3a;color:#67d4f6;}
   .cat.ip-impersonation{background:#3a1230;color:#f667c9;} .cat.website-tos{background:#12303a;color:#67f6c9;}
   .cat.other{background:#26262b;color:#b9b9c2;}
+  a.drill-proj { color:var(--dim); text-decoration:none; }
+  a.drill-proj:hover { color:var(--teal); text-decoration:underline; }
   .flag { font-size:10px; font-weight:700; padding:2px 7px; border-radius:5px; letter-spacing:.4px; }
   .flag.ok{background:#123a1e;color:#67f69a;} .flag.bl{background:#2a2410;color:#e0c766;}
   .prompt { font-size:14px; color:var(--ink); white-space:pre-wrap; word-break:break-word; }
@@ -113,13 +117,22 @@ function fmt(ts){ if(!ts) return 'unknown time'; const d=new Date(ts);
   return isNaN(d)? String(ts) : d.toLocaleString(undefined,{year:'numeric',month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}); }
 function esc(s){ return String(s==null?'':s).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c])); }
 
+// ── href-to-deeper-data primitives (page-native over view+q+sort+bl+cats) — HARD rule 2026-07-31 ──
+// Every category chip is a real, shareable href to its filtered view (deep-link + back-button).
+function buildQS(cats){ const u=new URLSearchParams(); if(VIEW&&VIEW!=='grid')u.set('view',VIEW); const q=el('q').value.trim(); if(q)u.set('q',q); const s=el('sort').value; if(s&&s!=='newest')u.set('sort',s); if(!el('borderline').checked)u.set('bl','0'); if(cats&&cats.size)u.set('cats',[...cats].join(',')); const o=u.toString(); return o?('?'+o):location.pathname; }
+function qstr(){ return buildQS(activeCats); }
+function catHref(c){ const next=new Set(activeCats); next.has(c)?next.delete(c):next.add(c); return buildQS(next); }
+function readURL(){ const p=new URLSearchParams(location.search); if(p.get('view'))VIEW=p.get('view'); if(p.has('q'))el('q').value=p.get('q'); if(p.get('sort'))el('sort').value=p.get('sort'); if(p.get('bl')==='0')el('borderline').checked=false; if(p.get('bl')==='1')el('borderline').checked=true; if(p.has('cats'))activeCats=new Set(p.get('cats').split(',').filter(Boolean)); }
+function writeURL(push){ history[push?'pushState':'replaceState']({},'',qstr()); }
+window.qstr=qstr; window.readURL=readURL; window.writeURL=writeURL; window.catHref=catHref;
+
 function filtered(){
   const q = el('q').value.trim().toLowerCase();
   const showBl = el('borderline').checked;
   let rows = ALL.filter(r => {
     if (!showBl && !r.confirmed) return false;
     if (activeCats.size && !activeCats.has(r.category)) return false;
-    if (q && !((r.user||'')+' '+(r.refusal||'')+' '+(r.reason||'')).toLowerCase().includes(q)) return false;
+    if (q && !((r.user||'')+' '+(r.refusal||'')+' '+(r.reason||'')+' '+(r.project||'')).toLowerCase().includes(q)) return false;
     return true;
   });
   const sort = el('sort').value;
@@ -132,13 +145,24 @@ function filtered(){
 }
 
 function badges(r){
-  return `<span class="cat ${r.category}">${esc(r.category)}</span>`+
+  // category badge is a drill link — href is shareable; plain-click toggles filter in-place
+  return `<a class="cat ${r.category} drill" href="${catHref(r.category)}" data-drill-cat="${esc(r.category)}" title="Filter to ${esc(r.category)}">${esc(r.category)}</a>`+
          (r.confirmed?`<span class="flag ok">✓ REFUSAL</span>`:`<span class="flag bl">? BORDERLINE</span>`)+
          (r.net==='B'?`<span class="flag bl" title="caught by tone, no policy keyword">TONE</span>`:'');
 }
+function projectHref(proj){
+  const u=new URLSearchParams(); if(VIEW&&VIEW!=='grid')u.set('view',VIEW); if(proj)u.set('q',proj);
+  const s=el('sort').value; if(s&&s!=='newest')u.set('sort',s); if(!el('borderline').checked)u.set('bl','0');
+  if(activeCats.size)u.set('cats',[...activeCats].join(','));
+  const o=u.toString(); return o?('?'+o):location.pathname;
+}
 function metaRow(r){
+  const proj = r.project||'';
+  const projEl = proj
+    ? `<a href="${projectHref(proj)}" class="drill-proj" data-drill-proj="${esc(proj)}" title="Search project: ${esc(proj)}">📁 ${esc(proj)}</a>`
+    : `<span>📁 —</span>`;
   return `<div class="meta"><span class="when" title="${esc(r.ts)}">🕓 ${fmt(r.ts)}</span>`+
-         `<span>📁 ${esc(r.project||'—')}</span><span class="src">${esc(r.file||'')}</span></div>`;
+         projEl+`<span class="src">${esc(r.file||'')}</span></div>`;
 }
 
 function renderGrid(rows){
@@ -168,9 +192,9 @@ function renderTable(rows){
   el('tbody').innerHTML = rows.map(r=>`
     <tr class="${r.confirmed?'':'borderline'}">
       <td class="when" title="${esc(r.ts)}">${fmt(r.ts)}</td>
-      <td><span class="cat ${r.category}">${esc(r.category)}</span></td>
+      <td><a class="cat ${r.category} drill" href="${catHref(r.category)}" data-drill-cat="${esc(r.category)}" title="Filter to ${esc(r.category)}">${esc(r.category)}</a></td>
       <td>${r.confirmed?'<span class="flag ok">✓</span>':'<span class="flag bl">?</span>'}</td>
-      <td>${esc(r.project||'—')}</td>
+      <td>${r.project?`<a href="${projectHref(r.project)}" class="drill-proj" data-drill-proj="${esc(r.project)}" title="Search project: ${esc(r.project)}">${esc(r.project)}</a>`:'—'}</td>
       <td class="tprompt"><div class="clamp">${esc(r.user)}</div></td>
       <td class="tref"><div class="clamp">${esc(r.refusal)}</div></td>
       <td>${esc(r.reason)}</td>
@@ -178,6 +202,7 @@ function renderTable(rows){
 }
 
 function render(){
+  try { writeURL(false); } catch(_) {}
   const rows = filtered();
   const conf = rows.filter(r=>r.confirmed).length;
   el('count').textContent = `· ${rows.length} shown (${conf} confirmed / ${rows.length-conf} borderline) · ${ALL.length} candidates total`;
@@ -191,8 +216,10 @@ function render(){
 
 function buildCatChips(){
   const cats = [...new Set(ALL.map(r=>r.category))].sort();
-  el('cats').innerHTML = cats.map(c=>`<span class="chip" data-c="${c}">${c} <b>${ALL.filter(r=>r.category===c).length}</b></span>`).join('');
-  el('cats').querySelectorAll('.chip').forEach(ch=>ch.onclick=()=>{
+  el('cats').innerHTML = cats.map(c=>`<a class="chip drill${activeCats.has(c)?' on':''}" data-filter="1" href="${catHref(c)}" data-c="${c}">${c} <b>${ALL.filter(r=>r.category===c).length}</b></a>`).join('');
+  el('cats').querySelectorAll('.chip').forEach(ch=>ch.onclick=(e)=>{
+    if(e.metaKey||e.ctrlKey)return; // let cmd/ctrl-click open the deep-link in a new tab
+    e.preventDefault();
     const c=ch.dataset.c;
     if(activeCats.has(c)){activeCats.delete(c);ch.classList.remove('on');}
     else{activeCats.add(c);ch.classList.add('on');}
@@ -229,6 +256,29 @@ function restore(){
   if(p.get('bl')==='1') el('borderline').checked=true;
 }
 
+// ── drill delegated click handlers ──
+// .cat.drill → toggle category filter in-place (cmd/ctrl-click = new tab via href)
+document.addEventListener('click', e=>{
+  const a = e.target.closest('a[data-drill-cat]');
+  if(!a) return;
+  if(e.metaKey||e.ctrlKey||e.button===1) return;
+  e.preventDefault();
+  const c = a.dataset.drillCat;
+  if(activeCats.has(c)){activeCats.delete(c);}else{activeCats.add(c);}
+  buildCatChips(); render();
+});
+// .drill-proj → set search to project name and re-render
+document.addEventListener('click', e=>{
+  const a = e.target.closest('a[data-drill-proj]');
+  if(!a) return;
+  if(e.metaKey||e.ctrlKey||e.button===1) return;
+  e.preventDefault();
+  const proj = a.dataset.drillProj;
+  if(!proj) return;
+  el('q').value = proj;
+  render();
+});
+
 restore();
 fetch(location.origin + '/api/rejections').then(r=>r.json()).then(d=>{
   ALL = d.items||[];

← 6e9367e auto-data-snapshot: 2026-08-24T04:13:07 (1 data files) — dat  ·  back to Rejected Prompts Viewer  ·  rejected-prompts-viewer: href-to-deeper-data — category chip 05dd469 →