[object Object]

← back to Wallco Ai

cactus-curator: 'By-base view' toggle — 1 pattern + thumbs of all colorways (overlay), ←/→/Esc + click-thumb-to-swap; backed by new /api/admin/cactus/families endpoint (route-guard /cactus/:id for shadowing)

e9fe6eb053f447e14edb21bc57f1ce7131a8187a · 2026-05-28 06:44:40 -0700 · Steve Abrams

Files touched

Diff

commit e9fe6eb053f447e14edb21bc57f1ce7131a8187a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 28 06:44:40 2026 -0700

    cactus-curator: 'By-base view' toggle — 1 pattern + thumbs of all colorways (overlay), ←/→/Esc + click-thumb-to-swap; backed by new /api/admin/cactus/families endpoint (route-guard /cactus/:id for shadowing)
---
 public/admin/cactus-curator.html | 101 +++++++++++++++++++++++++++++++++++++++
 1 file changed, 101 insertions(+)

diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index abc888c..867efcc 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -747,6 +747,107 @@ $('collection').addEventListener('change', e => {
 
 loadCollections();
 load();
+
+// ===== "By-base view" toggle =====================================================
+// One pattern at a time, with thumbs of all its colorway variants below it. Powers
+// fast colorway eyeballing without scrolling a giant grid. Self-contained overlay
+// (fixed-position) — touches no existing markup; toggleable from a header button.
+(() => {
+  const URL_CAT = new URLSearchParams(location.search).get('category') || '';
+  let families = [], idx = 0, mainId = null, overlay = null;
+
+  function ensureOverlay() {
+    if (overlay) return overlay;
+    overlay = document.createElement('div'); overlay.id = 'bbview';
+    overlay.style.cssText = 'position:fixed;inset:0;z-index:150;background:rgba(8,9,11,.96);display:none;flex-direction:column;color:#eee;padding:14px;font:13px -apple-system,sans-serif;';
+    overlay.innerHTML = `
+      <div style="display:flex;align-items:center;gap:12px;border-bottom:1px solid #333;padding-bottom:8px;margin-bottom:12px">
+        <button id="bbprev" style="background:#2a2a2a;color:#eee;border:1px solid #444;padding:7px 14px;font-weight:700;border-radius:8px;cursor:pointer">‹ Prev</button>
+        <span id="bbpos" style="font-variant-numeric:tabular-nums;color:#aaa;min-width:200px;text-align:center"></span>
+        <button id="bbnext" style="background:#2a2a2a;color:#eee;border:1px solid #444;padding:7px 14px;font-weight:700;border-radius:8px;cursor:pointer">Next ›</button>
+        <span style="flex:1;text-align:center;color:#8a929a">${URL_CAT} · by-base view · ←/→/Esc</span>
+        <button id="bbclose" style="background:#3a2222;color:#ffdada;border:1px solid #663;padding:7px 14px;font-weight:700;border-radius:8px;cursor:pointer">✕ Close</button>
+      </div>
+      <div id="bbmain" style="flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center"></div>
+      <div id="bbthumbs" style="margin-top:10px"></div>`;
+    document.body.appendChild(overlay);
+    overlay.querySelector('#bbclose').onclick = closeView;
+    overlay.querySelector('#bbprev').onclick = () => go(-1);
+    overlay.querySelector('#bbnext').onclick = () => go(1);
+    return overlay;
+  }
+
+  function renderView() {
+    if (!families.length) return;
+    const fam = families[idx];
+    const all = [
+      { id: fam.id, category: fam.category, dominant_hex: fam.dominant_hex, is_published: fam.is_published },
+      ...(fam.children || []),
+    ];
+    if (!mainId || !all.find(x => x.id === mainId)) mainId = fam.id;
+    const main = all.find(x => x.id === mainId);
+    overlay.querySelector('#bbpos').textContent = `${idx + 1} / ${families.length}  ·  base #${fam.id}  ·  ${all.length} colorways`;
+    overlay.querySelector('#bbmain').innerHTML =
+      `<img src="/designs/img/by-id/${main.id}" style="display:block;margin:0 auto;max-width:min(92vw,820px);max-height:62vh;background:#fff;object-fit:contain;box-shadow:0 6px 30px rgba(0,0,0,.5);border-radius:6px">
+       <div style="text-align:center;font-size:11px;color:#aaa;margin-top:6px;font-variant-numeric:tabular-nums">
+         #${main.id}  ·  ${main.category}  ·  ${main.is_published ? '✓ published' : '○ unpublished'}
+       </div>`;
+    overlay.querySelector('#bbthumbs').innerHTML =
+      `<div style="display:flex;gap:6px;overflow-x:auto;padding:8px 4px;justify-content:flex-start">` +
+      all.map(c => {
+        const cw = (c.category || '').split(' · ')[1] || 'base';
+        const sel = c.id === main.id;
+        return `<div class="bbthumb" data-id="${c.id}" style="flex:0 0 auto;cursor:pointer;border:2px solid ${sel ? '#ffd68a' : (c.is_published ? '#3a6a3a' : '#3a3a3a')};border-radius:6px;overflow:hidden;background:#222">
+          <img src="/designs/img/by-id/${c.id}" style="display:block;width:72px;height:72px;object-fit:cover" loading="lazy">
+          <div style="text-align:center;font-size:9px;padding:2px 4px;color:${sel ? '#ffd68a' : '#aaa'};max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${cw}</div>
+        </div>`;
+      }).join('') + `</div>`;
+    overlay.querySelectorAll('.bbthumb').forEach(t => t.onclick = () => { mainId = +t.dataset.id; renderView(); });
+  }
+
+  function go(delta) {
+    const n = idx + delta; if (n < 0 || n >= families.length) return;
+    idx = n; mainId = null; renderView();
+  }
+
+  function openView() {
+    ensureOverlay();
+    overlay.style.display = 'flex';
+    if (!families.length) {
+      overlay.querySelector('#bbmain').innerHTML = '<div style="text-align:center;padding:40px;color:#888">loading families…</div>';
+      fetch('/api/admin/cactus/families?category=' + encodeURIComponent(URL_CAT))
+        .then(r => r.json())
+        .then(j => {
+          families = (j.families || []).filter(f => (f.children || []).length > 0);
+          if (!families.length) overlay.querySelector('#bbmain').innerHTML = '<div style="text-align:center;padding:40px;color:#888">no bases with colorway children in this category</div>';
+          else { idx = 0; mainId = null; renderView(); }
+        })
+        .catch(e => overlay.querySelector('#bbmain').innerHTML = '<div style="color:#f88">load failed: ' + (e.message || e) + '</div>');
+    } else renderView();
+  }
+
+  function closeView() { if (overlay) overlay.style.display = 'none'; }
+
+  // inject toggle button into the controls bar (re-injects if removed)
+  new MutationObserver(() => {
+    if (document.getElementById('bbtoggle')) return;
+    const anchor = document.getElementById('autoall') || document.querySelector('.controls') || document.querySelector('header');
+    if (!anchor || !anchor.parentNode) return;
+    const b = document.createElement('button'); b.id = 'bbtoggle';
+    b.className = anchor.className || '';
+    b.textContent = '🎨 By-base view';
+    b.title = 'one pattern at a time + thumbs of all its colorways below';
+    b.onclick = openView;
+    anchor.parentNode.insertBefore(b, anchor.nextSibling);
+  }).observe(document.body, { childList: true, subtree: true });
+
+  addEventListener('keydown', e => {
+    if (!overlay || overlay.style.display !== 'flex') return;
+    if (e.key === 'Escape') closeView();
+    else if (e.key === 'ArrowLeft') go(-1);
+    else if (e.key === 'ArrowRight') go(1);
+  });
+})();
 </script>
 </body>
 </html>

← 3fc593c etsy-tag endpoint: drop nonexistent 'title' column (view has  ·  back to Wallco Ai  ·  tone-on-tone hex picker: sort by hue + collapse near-duplica c07a04e →