← back to Wallco Ai
Cactus curator: show created date+time per card; add marquee drag-select + shift-add
08ff7862022c7e90935227c24cc88f34fc1052b2 · 2026-05-27 08:30:47 -0700 · Steve Abrams
- each card now shows 🕓 created date & time (toLocaleString, tabular nums)
- drag across images = rubber-band marquee select; shift+drag adds to the
existing selection; shift+click toggles one card. Live highlight + bulk bar.
- images set -webkit-user-drag:none so native img-drag doesn't hijack marquee.
Verified headless: 672 cards render, marquee selects, date+time shows, no JS errors.
Files touched
M public/admin/cactus-curator.html
Diff
commit 08ff7862022c7e90935227c24cc88f34fc1052b2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 27 08:30:47 2026 -0700
Cactus curator: show created date+time per card; add marquee drag-select + shift-add
- each card now shows 🕓 created date & time (toLocaleString, tabular nums)
- drag across images = rubber-band marquee select; shift+drag adds to the
existing selection; shift+click toggles one card. Live highlight + bulk bar.
- images set -webkit-user-drag:none so native img-drag doesn't hijack marquee.
Verified headless: 672 cards render, marquee selects, date+time shows, no JS errors.
---
public/admin/cactus-curator.html | 63 ++++++++++++++++++++++++++++++++++++++--
1 file changed, 61 insertions(+), 2 deletions(-)
diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index e7a2673..50ecbc8 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -28,7 +28,7 @@
.card.decided { opacity:.5; }
.card.sel { outline:2px solid #4aa3ff; outline-offset:-2px; }
.thumb { position:relative; aspect-ratio:1/1; background:#000; }
- .thumb img { width:100%; height:100%; object-fit:cover; display:block; }
+ .thumb img { width:100%; height:100%; object-fit:cover; display:block; -webkit-user-drag:none; user-select:none; }
.sel-box { position:absolute; top:6px; left:6px; width:20px; height:20px; border-radius:5px; border:2px solid #fff;
background:rgba(0,0,0,.45); cursor:pointer; z-index:3; }
.card.sel .sel-box { background:#4aa3ff; border-color:#4aa3ff; }
@@ -44,6 +44,11 @@
.cw { font-size:12px; color:var(--ink); font-weight:600; text-transform:capitalize; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.scores { display:flex; align-items:center; gap:8px; margin-top:3px; color:var(--mut); font-size:11px; }
.dot { width:11px; height:11px; border-radius:50%; border:1px solid rgba(255,255,255,.25); display:inline-block; vertical-align:-1px; }
+ .when { margin-top:3px; color:var(--mut); font-size:10.5px; font-variant-numeric:tabular-nums; }
+ /* marquee rubber-band selection */
+ #marquee { position:fixed; z-index:40; border:1.5px solid #4aa3ff; background:rgba(74,163,255,.16); pointer-events:none; display:none; border-radius:3px; }
+ body.dragging { user-select:none; cursor:crosshair; }
+ body.dragging .card { cursor:crosshair; }
.acts { display:grid; grid-template-columns:1fr 1fr; gap:4px; padding:6px 8px 9px; }
.acts button { font:600 11px/1.2 inherit; padding:7px 4px; border-radius:7px; border:1px solid var(--line);
background:#1d2023; color:var(--ink); cursor:pointer; transition:filter .1s; }
@@ -98,13 +103,14 @@
<label class="ctl">Density
<input type="range" id="density" min="2" max="9" step="1" value="5">
</label>
- <span class="legend">keys when hovering a card: <code>1</code> bad · <code>2</code> digital · <code>3</code> fix · <code>4</code> live · <code>x</code> select</span>
+ <span class="legend"><b>drag</b> across images to select · <b>shift+drag</b> adds · <b>shift+click</b> a card to add one · keys on hover: <code>1</code>bad <code>2</code>digital <code>3</code>fix <code>4</code>live <code>x</code>select</span>
</div>
</header>
<div id="err" style="display:none"></div>
<div id="empty" style="display:none">No designs match this filter.</div>
<div class="grid" id="grid"></div>
+<div id="marquee"></div>
<div id="bulk">
<span><b id="bulkn">0</b> selected</span>
@@ -207,6 +213,7 @@ function card(d){
<span title="vision quality">V ${vis!=null?Math.round(vis):'·'}</span>
<span title="seam quality">S ${seam!=null?Math.round(seam):'·'}</span>
</div>
+ <div class="when" title="created ${d.created_at||''}">🕓 ${fmtDate(d.created_at)}</div>
</div>
<div class="acts">
<button class="bad" data-a="bad" title="Bad pattern — remove from all (1)">✕ Bad <kbd>1</kbd></button>
@@ -216,6 +223,11 @@ function card(d){
</div>`;
el.querySelector('.sel-box').addEventListener('click', e => { e.stopPropagation(); toggleSel(d.id, el); });
+ // shift+click anywhere on the image adds/removes this one card
+ el.querySelector('.thumb').addEventListener('click', e => {
+ if (e.target.classList.contains('sel-box')) return;
+ if (e.shiftKey) { e.preventDefault(); toggleSel(d.id, el); }
+ });
el.querySelectorAll('.acts button').forEach(b =>
b.addEventListener('click', () => decide(d.id, b.dataset.a, el)));
el.addEventListener('mouseenter', () => hovered = d.id);
@@ -294,8 +306,55 @@ document.addEventListener('keydown', e => {
});
function today(){ return new Date().toISOString().slice(0,10); }
+function fmtDate(iso){
+ if(!iso) return '—';
+ const d = new Date(iso); if(isNaN(d)) return '—';
+ return d.toLocaleString(undefined, { year:'numeric', month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
+}
function flash(msg){ const e=$('err'); e.textContent=msg; e.style.display='block'; setTimeout(()=>e.style.display='none', 4000); }
+// reconcile the live selection Set + .sel classes + bulk bar to a target set
+function applySelection(next){
+ for(const id of [...selected]) if(!next.has(id)){ selected.delete(id); grid.querySelector(`.card[data-id="${id}"]`)?.classList.remove('sel'); }
+ for(const id of next) if(!selected.has(id)){ selected.add(id); grid.querySelector(`.card[data-id="${id}"]`)?.classList.add('sel'); }
+ $('bulkn').textContent = selected.size;
+ $('bulk').classList.toggle('show', selected.size>0);
+}
+
+// ── marquee rubber-band select: drag = select, shift+drag = add ────────────
+(function(){
+ const mq = $('marquee'), THRESH = 5;
+ let dragging=false, moved=false, sx=0, sy=0, baseSel=null;
+ grid.addEventListener('mousedown', e => {
+ if(e.button!==0) return;
+ if(e.target.closest('.acts') || e.target.classList.contains('sel-box')) return; // not on buttons/checkbox
+ dragging=true; moved=false; sx=e.clientX; sy=e.clientY;
+ baseSel = e.shiftKey ? new Set(selected) : new Set();
+ });
+ window.addEventListener('mousemove', e => {
+ if(!dragging) return;
+ if(!moved && Math.abs(e.clientX-sx)<THRESH && Math.abs(e.clientY-sy)<THRESH) return;
+ if(!moved){
+ moved=true; document.body.classList.add('dragging'); mq.style.display='block';
+ if(baseSel.size===0) applySelection(new Set()); // fresh (non-shift) drag clears first
+ }
+ e.preventDefault();
+ const l=Math.min(sx,e.clientX), t=Math.min(sy,e.clientY), r=Math.max(sx,e.clientX), b=Math.max(sy,e.clientY);
+ mq.style.left=l+'px'; mq.style.top=t+'px'; mq.style.width=(r-l)+'px'; mq.style.height=(b-t)+'px';
+ const next = new Set(baseSel);
+ for(const cardEl of grid.children){
+ const cr = cardEl.getBoundingClientRect();
+ if(!(cr.right<l || cr.left>r || cr.bottom<t || cr.top>b)) next.add(+cardEl.dataset.id);
+ }
+ applySelection(next);
+ });
+ window.addEventListener('mouseup', () => {
+ if(!dragging) return;
+ dragging=false; moved=false; baseSel=null;
+ document.body.classList.remove('dragging'); mq.style.display='none';
+ });
+})();
+
async function load(){
try {
const r = await fetch(q('/api/admin/cactus/list?include=removed'));
← 63e2927 Purge 2,262 fuzzy-seam cactus tiles (start-fresh); add seam-
·
back to Wallco Ai
·
drunk-animals prompt surgery: kill poster/album/grid styles be027c7 →