← 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
A public/drill.jsM public/index.html
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=>({'&':'&','<':'<','>':'>'}[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 →