← back to Gated Queue Runner
add sortable Table view + Cards/Table toggle (every column click-to-sort)
5efaf263f79bce0a3e763b6dfea40cae35c3dab7 · 2026-08-18 13:22:41 -0700 · steve
Files touched
Diff
commit 5efaf263f79bce0a3e763b6dfea40cae35c3dab7
Author: steve <steve@designerwallcoverings.com>
Date: Tue Aug 18 13:22:41 2026 -0700
add sortable Table view + Cards/Table toggle (every column click-to-sort)
---
index.html | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++-----------
server.js | 9 ++++----
2 files changed, 67 insertions(+), 17 deletions(-)
diff --git a/index.html b/index.html
index d389305..435dc0f 100644
--- a/index.html
+++ b/index.html
@@ -32,6 +32,17 @@
.card .when { font-size: .72em; color: #8a93a8; display: flex; align-items: center; gap: 4px; }
.card .file { font-size: .68em; color: #aab; word-break: break-all; }
.card button { align-self: flex-start; margin-top: 2px; border: 1px solid #ccd3e0; background: #f7f9ff; border-radius: 8px; padding: 5px 10px; font-size: .8em; cursor: pointer; }
+ table { width: 100%; border-collapse: collapse; background: #fff; font-size: .9em; }
+ thead th { position: sticky; top: 0; background: #eef2fb; text-align: left; padding: 8px 10px; border-bottom: 2px solid #d3dae8; cursor: pointer; user-select: none; white-space: nowrap; }
+ thead th:hover { background: #e2e8f8; }
+ thead th .ar { color: #2d5bff; margin-left: 4px; }
+ tbody td { padding: 8px 10px; border-bottom: 1px solid #eef1f7; vertical-align: top; }
+ tbody tr:hover { background: #f7f9ff; }
+ tbody tr.sel { background: #eaf0ff; }
+ td.about { max-width: 380px; }
+ td.effect { color: #445; max-width: 260px; font-size: .92em; }
+ td.file { font-size: .78em; color: #99a; word-break: break-all; max-width: 220px; }
+ td .link { color: #2d5bff; cursor: pointer; text-decoration: underline; font-size: .85em; }
dialog { border: none; border-radius: 14px; max-width: 760px; width: 92vw; padding: 0; }
dialog .dh { padding: 12px 16px; border-bottom: 1px solid #eee; font-weight: 700; display: flex; justify-content: space-between; }
dialog pre { margin: 0; padding: 16px; white-space: pre-wrap; font-size: .82em; max-height: 70vh; overflow: auto; }
@@ -44,6 +55,9 @@
<div class="sub" id="sub">loading…</div>
<div class="controls">
<input type="search" id="q" placeholder="🔎 search…">
+ <label>View
+ <select id="view"><option value="cards">🃏 Cards</option><option value="table">▦ Table</option></select>
+ </label>
<label>Sort
<select id="sort">
<option value="new">Newest first</option>
@@ -56,7 +70,10 @@
</div>
<div class="chips" id="chips"></div>
</header>
-<main><div class="grid" id="grid"></div></main>
+<main>
+ <div class="grid" id="grid"></div>
+ <table id="tbl" style="display:none"><thead id="thead"></thead><tbody id="tbody"></tbody></table>
+</main>
<div id="selbar" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1c2333;color:#fff;padding:12px 18px;display:none;align-items:center;gap:14px;box-shadow:0 -2px 10px rgba(0,0,0,.2);z-index:20">
<span id="selcount" style="font-weight:700"></span>
<button id="savebtn" style="background:#2d5bff;color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:.95em;cursor:pointer">💾 Save my list → hand to Claude</button>
@@ -101,18 +118,31 @@ function renderChips(){
c.appendChild(el);
});
}
+let tSort={field:'created',dir:-1};
+function getRows(){
+ const q=$('q').value.toLowerCase();
+ return DATA.gates.filter(x=> (active.size===0||active.has(x.category)) &&
+ (!q || (x.about+x.effect+x.file+x.title+x.catLabel).toLowerCase().includes(q)));
+}
function render(){
- const q=$('q').value.toLowerCase(), sort=$('sort').value;
- let g=DATA.gates.filter(x=> (active.size===0||active.has(x.category)) &&
- (!q || (x.note+x.file+x.title).toLowerCase().includes(q)));
- if(sort==='new') g.sort((a,b)=>b.created-a.created);
- if(sort==='old') g.sort((a,b)=>a.created-b.created);
- if(sort==='az') g.sort((a,b)=>a.title.localeCompare(b.title));
- if(sort==='cat') g.sort((a,b)=>a.category.localeCompare(b.category)||b.created-a.created);
+ const view=$('view').value; let g=getRows();
+ if(view==='table'){
+ $('grid').style.display='none'; $('tbl').style.display='';
+ const key={kind:'catLabel',pick:'file'}[tSort.field]||tSort.field;
+ g.sort((a,b)=>{let va=a[key],vb=b[key]; if(typeof va==='string'){va=va.toLowerCase();vb=(vb||'').toLowerCase();} return (va<vb?-1:va>vb?1:0)*tSort.dir;});
+ renderTable(g);
+ } else {
+ $('tbl').style.display='none'; $('grid').style.display='';
+ const s=$('sort').value;
+ if(s==='new')g.sort((a,b)=>b.created-a.created); else if(s==='old')g.sort((a,b)=>a.created-b.created);
+ else if(s==='az')g.sort((a,b)=>a.title.localeCompare(b.title)); else if(s==='cat')g.sort((a,b)=>a.category.localeCompare(b.category)||b.created-a.created);
+ renderCards(g);
+ }
+}
+function renderCards(g){
const grid=$('grid'); grid.innerHTML='';
g.forEach(x=>{
- const d=document.createElement('div'); d.className='card';
- const on = SEL.has(x.file);
+ const d=document.createElement('div'); d.className='card'; const on=SEL.has(x.file);
d.innerHTML=`<div class="top"><label class="pick"><input type="checkbox" ${on?'checked':''}> pick</label><span class="emoji">${x.emoji}</span><span class="cat">${x.catLabel}</span></div>
<div class="note">${esc(x.note)}</div>
<div class="when">🕓 ${fmtWhen(x.created)}</div>
@@ -125,14 +155,33 @@ function render(){
});
if(!g.length) grid.innerHTML='<p style="color:#889">Nothing matches.</p>';
}
+const COLS=[{f:'pick',l:'✔'},{f:'kind',l:'Kind'},{f:'about',l:"What it's about"},{f:'effect',l:'If you say YES'},{f:'created',l:'When'},{f:'file',l:'File'}];
+function renderTable(g){
+ $('thead').innerHTML='<tr>'+COLS.map(c=>{const ar=tSort.field===c.f?`<span class="ar">${tSort.dir>0?'▲':'▼'}</span>`:'';return `<th data-f="${c.f}">${c.l}${ar}</th>`;}).join('')+'</tr>';
+ $('thead').querySelectorAll('th').forEach(th=>th.onclick=()=>{const f=th.dataset.f; if(tSort.field===f)tSort.dir*=-1; else{tSort.field=f;tSort.dir=(f==='created'?-1:1);} render();});
+ const tb=$('tbody'); tb.innerHTML='';
+ g.forEach(x=>{
+ const tr=document.createElement('tr'); if(SEL.has(x.file))tr.className='sel';
+ tr.innerHTML=`<td><input type="checkbox" ${SEL.has(x.file)?'checked':''}></td>
+ <td style="white-space:nowrap">${x.emoji} ${esc(x.catLabel)}</td>
+ <td class="about">${esc(x.about)}</td>
+ <td class="effect">${esc(x.effect)}</td>
+ <td style="white-space:nowrap">${fmtWhen(x.created)}</td>
+ <td class="file">${esc(x.file)} <span class="link">open</span></td>`;
+ tr.querySelector('input').onchange=e=>{ e.target.checked?SEL.add(x.file):SEL.delete(x.file); tr.classList.toggle('sel',e.target.checked); saveSel(); bar(); };
+ tr.querySelector('.link').onclick=()=>openMemo(x.file,x.title);
+ tb.appendChild(tr);
+ });
+ if(!g.length) tb.innerHTML='<tr><td colspan="6" style="color:#889">Nothing matches.</td></tr>';
+}
function openMemo(f,t){ fetch('/api/memo?f='+encodeURIComponent(f)).then(r=>r.text()).then(txt=>{ $('dt').textContent=t; $('dp').textContent=txt; $('dlg').showModal(); }); }
function esc(s){ return (s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])); }
// controls + persistence
-['q','sort'].forEach(id=>$(id).addEventListener('input',()=>{save();render();}));
+['q','sort','view'].forEach(id=>$(id).addEventListener('input',()=>{save();render();}));
$('dens').addEventListener('input',e=>{ const v=+e.target.value; document.documentElement.style.setProperty('--cols', 6-v); document.documentElement.style.setProperty('--fs', (12+v*2)+'px'); localStorage.gqDens=v; });
-function save(){ localStorage.gqSort=$('sort').value; localStorage.gqQ=$('q').value; }
-(function restore(){ if(localStorage.gqSort)$('sort').value=localStorage.gqSort; if(localStorage.gqQ)$('q').value=localStorage.gqQ;
+function save(){ localStorage.gqSort=$('sort').value; localStorage.gqQ=$('q').value; localStorage.gqView=$('view').value; }
+(function restore(){ if(localStorage.gqSort)$('sort').value=localStorage.gqSort; if(localStorage.gqQ)$('q').value=localStorage.gqQ; if(localStorage.gqView)$('view').value=localStorage.gqView;
const v=localStorage.gqDens||3; $('dens').value=v; document.documentElement.style.setProperty('--cols',6-v); document.documentElement.style.setProperty('--fs',(12+v*2)+'px'); })();
load();
</script>
diff --git a/server.js b/server.js
index f88f233..3025394 100644
--- a/server.js
+++ b/server.js
@@ -91,10 +91,9 @@ function extractSummary(body) {
}
return null;
}
-function kidNote(cat, title, body) {
+function aboutText(cat, title, body) {
const sum = extractSummary(body);
- const about = sum || simplify(title);
- return `This is about: ${about} ${cat.yes}`;
+ return sum || simplify(title);
}
function listGates() {
@@ -109,9 +108,11 @@ function listGates() {
const st = fs.statSync(p);
const cat = categorize(body);
const title = cleanTitle(first, f);
+ const about = aboutText(cat, title, body);
return {
file: f, title, category: cat.key, catLabel: cat.label, emoji: cat.emoji,
- note: kidNote(cat, title, body), created: st.mtimeMs, size: st.size,
+ about, effect: cat.yes, note: `This is about: ${about} ${cat.yes}`,
+ created: st.mtimeMs, size: st.size,
};
}).sort((a, b) => b.created - a.created);
}
← bf673da quality-gate note extraction (prose only, else title)
·
back to Gated Queue Runner
·
GO actions: Allow/Never-run(archive)/Run-now(iTerm2 per item dac79e4 →