[object Object]

← back to Wallco Ai

cactus-curator: select-all button + live-animate-out + auto-force-publish UX

6f2666daaedcc027b5e09fc7d4a1b0cbe73680ad · 2026-05-29 13:23:17 -0700 · Steve Abrams

three small UX upgrades on the same surface, in flight since the earlier
'debug why i cant publish anything' + 'allow user to select 1 to all
in check marks' interrupts.

1. select-all toolbar button — '☑ Select all visible' next to Auto-ID.
   Click once: every visible card gets sel-class + adds to selected set
   and bulk-bar appears. Click again (or once all selected): clears.
   Skips .moved-to-etsy mid-animation cards so a recent decision doesn't
   sneak back into the next bulk action.

2. Publish on gate-fail now visually completes — added 'live' to the
   per-card animate-out action set so when the auto-force-publish lands,
   the card fades out (was sitting there with ✓ PUBLISHED pill while
   Steve was puzzled why nothing visibly happened). The local model is
   patched (is_published=true, web_viewer=true, user_removed=false,
   needs_fixing_at=null) inside the force-success branch so filter
   re-renders won't bring it back.

3. force-publish success branch in decide() now also animates out the
   element after patching the local model — matching the gate-pass live
   path. Without this the 'auto-force-publishing…' toast shows, the row
   flips in PG, the local model updates, but the card sits there
   indefinitely with ✓ PUBLISHED until you refresh.

verified locally + on prod: HTML serves 90679 bytes, all 4 new code chunks
present in prod-served response.

Files touched

Diff

commit 6f2666daaedcc027b5e09fc7d4a1b0cbe73680ad
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri May 29 13:23:17 2026 -0700

    cactus-curator: select-all button + live-animate-out + auto-force-publish UX
    
    three small UX upgrades on the same surface, in flight since the earlier
    'debug why i cant publish anything' + 'allow user to select 1 to all
    in check marks' interrupts.
    
    1. select-all toolbar button — '☑ Select all visible' next to Auto-ID.
       Click once: every visible card gets sel-class + adds to selected set
       and bulk-bar appears. Click again (or once all selected): clears.
       Skips .moved-to-etsy mid-animation cards so a recent decision doesn't
       sneak back into the next bulk action.
    
    2. Publish on gate-fail now visually completes — added 'live' to the
       per-card animate-out action set so when the auto-force-publish lands,
       the card fades out (was sitting there with ✓ PUBLISHED pill while
       Steve was puzzled why nothing visibly happened). The local model is
       patched (is_published=true, web_viewer=true, user_removed=false,
       needs_fixing_at=null) inside the force-success branch so filter
       re-renders won't bring it back.
    
    3. force-publish success branch in decide() now also animates out the
       element after patching the local model — matching the gate-pass live
       path. Without this the 'auto-force-publishing…' toast shows, the row
       flips in PG, the local model updates, but the card sits there
       indefinitely with ✓ PUBLISHED until you refresh.
    
    verified locally + on prod: HTML serves 90679 bytes, all 4 new code chunks
    present in prod-served response.
---
 public/admin/cactus-curator.html | 40 +++++++++++++++++++++++++++++++++-------
 1 file changed, 33 insertions(+), 7 deletions(-)

diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index 27f8a96..d59a58d 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -201,6 +201,7 @@
       <input type="range" id="density" min="2" max="9" step="1" value="5">
     </label>
     <button id="autoall" class="ctl-btn" title="auto-detect bad areas on every visible card">🔍 Auto-ID all visible</button>
+    <button id="selectall" class="ctl-btn" title="Toggle select all visible cards — click again to clear">☑ Select all visible</button>
     <button id="toggleacts" class="ctl-btn" title="show/hide the Bad/Digital/Fix/Publish buttons on every card (they always show on hover)">▸ Actions</button>
     <span class="legend"><b>drag</b> across images to select · <b>shift+drag on an image</b> = ▢ box a bad area (red) · <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>publish <code>x</code>select</span>
   </div>
@@ -268,6 +269,25 @@ $('sort').value = LS.s;
 $('filter').value = LS.f;
 
 $('density').addEventListener('input', e => { LS.d = e.target.value; applyDensity(e.target.value); });
+
+// Select-all toggle (Steve 2026-05-29 "allow user to select 1 to all in check marks").
+// First click selects every visible card; second click clears the selection. Animated
+// cards (already 'moved-to-etsy' transitioning out) skipped so a recent Bad/Publish
+// doesn't quietly come back into the selection set.
+$('selectall').addEventListener('click', () => {
+  const cards = [...grid.querySelectorAll('.card[data-id]:not(.moved-to-etsy)')];
+  if (!cards.length) return;
+  const allSelected = cards.every(c => c.classList.contains('sel'));
+  if (allSelected) {
+    cards.forEach(c => { selected.delete(parseInt(c.dataset.id,10)); c.classList.remove('sel'); });
+  } else {
+    cards.forEach(c => { const id = parseInt(c.dataset.id,10); if (!isNaN(id)) { selected.add(id); c.classList.add('sel'); } });
+  }
+  $('bulkn').textContent = selected.size;
+  $('bulk').classList.toggle('show', selected.size > 0);
+  $('selectall').textContent = (selected.size && selected.size === cards.length) ? '✕ Clear selection' : '☑ Select all visible';
+});
+
 // Auto-ID every visible card (capped, limited concurrency so we don't hammer Gemini).
 $('autoall').addEventListener('click', async () => {
   const cards = [...grid.querySelectorAll('.card')];
@@ -676,6 +696,11 @@ async function decide(id, action, el){
           if(st){st.textContent='✓ PUBLISHED';}
           const dd=ALL.find(x=>x.id===id);
           if(dd){dd.is_published=true; dd.web_viewer=true; dd.user_removed=false; dd.needs_fixing_at=null; dd.digital_file_at=null;}
+          // Steve 2026-05-29: animate card out on successful publish so it leaves
+          // the Undecided filter view (matches bad/digital/unpublish behavior).
+          // Without this the card sits there with ✓ PUBLISHED and Steve sees
+          // 'auto-force-publishing…' but no visible action — feels broken.
+          if(el){ el.classList.add('moved-to-etsy'); setTimeout(()=>el.remove(),300); }
         } else {
           flash(`#${id} force-publish failed: ${j2.error||r2.status}`);
         }
@@ -686,13 +711,14 @@ async function decide(id, action, el){
       // button land inside the card. Original PNG is backed up alongside as
       // __preseam.png — restore-preseam reverses the swap.
       runFixAndDisplay(id, el);
-    } else if (['bad','digital','unpublish'].includes(action) && el) {
-      // Steve 2026-05-29: per-card Bad/Digital/Unpublish now animate the card
-      // out of the grid on success (mirrors bulk's 300ms fade+remove). Without
-      // this the card sits there with a REMOVED pill while the SQL update has
-      // already landed, making the click feel broken. Etsy + Live skip this
-      // because Etsy already self-hides via its own path and Live keeps the
-      // card visible (now-published, no reason to drop it from the queue).
+    } else if (['bad','digital','unpublish','live'].includes(action) && el) {
+      // Steve 2026-05-29: per-card Bad/Digital/Unpublish/Publish now animate
+      // the card out of the grid on success (mirrors bulk's 300ms fade+remove).
+      // Without this the card sits there with its new state-pill while the SQL
+      // update already landed, making the click feel broken. Live was added
+      // 2026-05-29 because Steve was hitting 'auto-force-publishing…' but
+      // seeing no visible action — the card was sitting there published but
+      // still in the Undecided view. Etsy keeps its own self-hide path.
       el.classList.add('moved-to-etsy');
       setTimeout(() => el.remove(), 300);
     }

← 8e67419 Add color-dots.js: vertical strip of the pattern's screen-pr  ·  back to Wallco Ai  ·  color-dots: interactive recolor — click a dot to open an HSV 69f306b →