[object Object]

← 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

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 →