← back to Wallco Ai
cactus-curator by-base: full selection + action parity with index — sel-box, .acts (Bad/Digital/Fix/Live/Etsy), state label, decided class. Delegated click handler on #bbstack survives renderStack innerHTML rebuilds. applySelection/toggleSel resolve cards across #grid AND #bbstack. Marquee drag-select binds to both containers + enumerates cards in either to extend rubber-band across the stack
ade52372008b03d383063000ee33f02554c3e517 · 2026-05-28 07:13:46 -0700 · Steve Abrams
Files touched
M public/admin/cactus-curator.htmlM public/admin/seam-debug.html
Diff
commit ade52372008b03d383063000ee33f02554c3e517
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 07:13:46 2026 -0700
cactus-curator by-base: full selection + action parity with index — sel-box, .acts (Bad/Digital/Fix/Live/Etsy), state label, decided class. Delegated click handler on #bbstack survives renderStack innerHTML rebuilds. applySelection/toggleSel resolve cards across #grid AND #bbstack. Marquee drag-select binds to both containers + enumerates cards in either to extend rubber-band across the stack
---
public/admin/cactus-curator.html | 90 ++++++++++++++++++++++++++++++++++++----
public/admin/seam-debug.html | 9 +++-
2 files changed, 89 insertions(+), 10 deletions(-)
diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index 2377d0a..6bf2807 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -488,6 +488,10 @@ function updateStat(){
}
function toggleSel(id, el){
+ // Resolve the card element across BOTH the main grid AND the by-base stack
+ // (#bbstack). When toggled from a delegated handler `el` is passed; on
+ // keyboard 'x' from any hovered card we look it up here.
+ el = el || document.querySelector(`#grid .card[data-id="${id}"], #bbstack .card[data-id="${id}"]`);
if(selected.has(id)){ selected.delete(id); el?.classList.remove('sel'); }
else { selected.add(id); el?.classList.add('sel'); }
$('bulkn').textContent = selected.size;
@@ -663,22 +667,42 @@ $('modal').addEventListener('click', e => { if(e.target.id==='modal') closeDetai
// 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'); }
+ // Find the card in EITHER the flat grid (#grid) or the by-base stack
+ // (#bbstack) so marquee-select works in both layouts.
+ const findCard = id => document.querySelector(`#grid .card[data-id="${id}"], #bbstack .card[data-id="${id}"]`);
+ for(const id of [...selected]) if(!next.has(id)){ selected.delete(id); findCard(id)?.classList.remove('sel'); }
+ for(const id of next) if(!selected.has(id)){ selected.add(id); findCard(id)?.classList.add('sel'); }
$('bulkn').textContent = selected.size;
$('bulk').classList.toggle('show', selected.size>0);
}
// ── marquee rubber-band select: drag = select, shift+drag = add ────────────
+// Works across BOTH the flat grid (#grid) and the by-base family stack
+// (#bbstack). Mousedown can start on either; mousemove enumerates every
+// .card in either container and adds any that intersect the marquee rect.
(function(){
const mq = $('marquee'), THRESH = 5;
let dragging=false, moved=false, sx=0, sy=0, baseSel=null;
- grid.addEventListener('mousedown', e => {
+ function startDrag(e){
if(e.button!==0) return;
- if(e.target.closest('.acts') || e.target.classList.contains('sel-box')) return; // not on buttons/checkbox
+ // Don't start a drag on action buttons, the sel-box, or the colorway
+ // thumb strip (.bbstrip — those <a>s are navigation, not selection).
+ if(e.target.closest('.acts') || e.target.classList.contains('sel-box') || e.target.closest('.bbstrip')) return;
dragging=true; moved=false; sx=e.clientX; sy=e.clientY;
baseSel = e.shiftKey ? new Set(selected) : new Set();
- });
+ }
+ grid.addEventListener('mousedown', startDrag);
+ // Attach to the stack TOO — it's created later by ensureStack(), so we use
+ // a MutationObserver to bind once it appears (and only once per session).
+ function bindStack(){
+ const stack = document.getElementById('bbstack');
+ if (!stack || stack.__mqBound) return;
+ stack.addEventListener('mousedown', startDrag);
+ stack.__mqBound = true;
+ }
+ bindStack();
+ new MutationObserver(bindStack).observe(document.body, { childList:true, subtree:true });
+
window.addEventListener('mousemove', e => {
if(!dragging) return;
if(!moved && Math.abs(e.clientX-sx)<THRESH && Math.abs(e.clientY-sy)<THRESH) return;
@@ -690,7 +714,10 @@ function applySelection(next){
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){
+ // Enumerate cards in both containers — whichever is visible contributes.
+ const cards = [...grid.querySelectorAll('.card[data-id]'),
+ ...document.querySelectorAll('#bbstack .card[data-id]')];
+ for(const cardEl of cards){
const cr = cardEl.getBoundingClientRect();
if(!(cr.right<l || cr.left>r || cr.bottom<t || cr.top>b)) next.add(+cardEl.dataset.id);
}
@@ -798,15 +825,22 @@ load();
// colorway pills inline in the meta row + a thumb strip where .acts goes.
const cwLabel = (fam.category || 'base');
const dot = fam.dominant_hex || '#333';
- return `<div class="card bbfam" data-base="${fam.id}" data-id="${fam.id}">
+ // Reflect any in-session decision on the base id so a re-render after a
+ // bulk action shows the right state.
+ const act = (typeof decided !== 'undefined' && decided.get) ? decided.get(fam.id) : null;
+ const selClass = (typeof selected !== 'undefined' && selected.has && selected.has(fam.id)) ? ' sel' : '';
+ const stateLabel = act ? ({bad:'✕ REMOVED',digital:'⬇ DIGITAL FILE',fix:'⚠ NEEDS FIXING',live:'✓ PUBLISHED',etsy:'🛒 ETSY BUCKET'})[act] : '';
+ return `<div class="card bbfam${act?' decided':''}${selClass}" data-base="${fam.id}" data-id="${fam.id}">
<div class="thumb">
+ <div class="sel-box" title="select (x)"></div>
<div class="badges">
<span class="b ${fam.is_published?'pub':'unpub'}">${fam.is_published?'PUB':'unpub'}</span>
<span class="b reason" title="${all.length} colorways in family">${all.length}cw</span>
</div>
- <a href="/admin/seam-debug/${fam.id}" target="_blank" style="display:block;width:100%;height:100%">
+ <a class="bb-link" href="/admin/seam-debug/${fam.id}" target="_blank" style="display:block;width:100%;height:100%">
<img loading="lazy" src="/designs/img/by-id/${fam.id}" alt="base ${fam.id}">
</a>
+ <div class="state" style="${stateLabel?'display:block':''}">${stateLabel}</div>
</div>
<div class="meta">
<div class="cw" title="${fam.category||''}">base <span class="sub">#${fam.id}</span></div>
@@ -825,9 +859,48 @@ load();
</a>`;
}).join('')}
</div>
+ <div class="acts">
+ <button class="bad" data-a="bad" title="Bad pattern — remove from all (1)">✕ Bad <kbd>1</kbd></button>
+ <button class="dig" data-a="digital" title="Unpublish, sell as digital file (2)">⬇ Digital <kbd>2</kbd></button>
+ <button class="fix" data-a="fix" title="Keep but needs fixing (3)">⚠ Fix <kbd>3</kbd></button>
+ <button class="live" data-a="live" title="Publish — go live in the web viewer (4)">✓ Publish <kbd>4</kbd></button>
+ <button class="etsy" data-a="etsy" title="Send to Etsy bucket (5)">🛒 Etsy <kbd>5</kbd></button>
+ </div>
</div>`;
}
+ // Delegated handlers on the stack — survives every renderStack() innerHTML
+ // rebuild. sel-box → toggleSel; .acts button → decide; shift-click thumb →
+ // toggleSel (mirrors the index page's shift-click-to-select).
+ function bindStackHandlers() {
+ if (stackEl.__handlersBound) return;
+ stackEl.addEventListener('click', e => {
+ const card = e.target.closest('.card.bbfam');
+ if (!card) return;
+ const id = +card.dataset.id;
+ // sel-box → selection toggle
+ if (e.target.classList.contains('sel-box')) {
+ e.preventDefault(); e.stopPropagation();
+ if (typeof toggleSel === 'function') toggleSel(id, card);
+ return;
+ }
+ // action button → decide()
+ const actBtn = e.target.closest('.acts button[data-a]');
+ if (actBtn) {
+ e.preventDefault(); e.stopPropagation();
+ if (typeof decide === 'function') decide(id, actBtn.dataset.a, card);
+ return;
+ }
+ // shift+click on the main thumb img → toggle select (NOT open the link)
+ if (e.shiftKey && e.target.closest('.bb-link')) {
+ e.preventDefault(); e.stopPropagation();
+ if (typeof toggleSel === 'function') toggleSel(id, card);
+ return;
+ }
+ }, true); // capture so we beat the <a>'s default navigation when needed
+ stackEl.__handlersBound = true;
+ }
+
function renderStack() {
if (!stackEl) ensureStack();
// When "Published only" is on, drop families that have zero published rows
@@ -842,6 +915,7 @@ load();
return;
}
stackEl.innerHTML = visible.map(familyCardHTML).join('');
+ bindStackHandlers();
}
function showStack() {
diff --git a/public/admin/seam-debug.html b/public/admin/seam-debug.html
index c672911..36180f5 100644
--- a/public/admin/seam-debug.html
+++ b/public/admin/seam-debug.html
@@ -95,8 +95,13 @@ $('t_seam').addEventListener('change',redraw);
addEventListener('keydown',e=>{ if(e.key==='Enter'&&document.activeElement===$('idin')) load(); });
let CUR=null;
-let SEL=new Set(); // selected box indices for healing
-function updHeal(){ const n=SEL.size; const b=$('healbtn'); b.disabled=n===0; b.textContent=`⚕ Heal selected (${n})`; }
+let SEL=new Set(); // selected AUTO-DETECTED box indices for healing
+let MANUAL=[]; // user-drawn boxes {x,y,w,h} in fractions (0..1) — always healed
+function updHeal(){
+ const n = SEL.size + MANUAL.length;
+ const b = $('healbtn'); b.disabled = n === 0;
+ b.textContent = `⚕ Heal selected (${n})${MANUAL.length ? ' · ' + MANUAL.length + ' manual' : ''}`;
+}
function lensRow(name, obj){
const score = obj.score!=null?obj.score:obj;
← 78f5533 cactus-curator by-base: 'Published only' filter (default ON)
·
back to Wallco Ai
·
cactus-curator: click-any-image → full-screen heal modal (dr 09b88ed →