← back to Wallco Ai
cactus-curator: 'By-base layout' is now an INDEX-PAGE mode (replaces modal overlay) — vertical stack of family cards (base big + colorway thumbs strip under each) for any ?category=, persists in localStorage, works on every collection with parent_design_id linkage
54a491e0d667c96853f69f548ae18946d75e62dd · 2026-05-28 06:55:16 -0700 · Steve Abrams
Files touched
M public/admin/cactus-curator.html
Diff
commit 54a491e0d667c96853f69f548ae18946d75e62dd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu May 28 06:55:16 2026 -0700
cactus-curator: 'By-base layout' is now an INDEX-PAGE mode (replaces modal overlay) — vertical stack of family cards (base big + colorway thumbs strip under each) for any ?category=, persists in localStorage, works on every collection with parent_design_id linkage
---
public/admin/cactus-curator.html | 157 ++++++++++++++++++++-------------------
1 file changed, 82 insertions(+), 75 deletions(-)
diff --git a/public/admin/cactus-curator.html b/public/admin/cactus-curator.html
index 867efcc..32ecb10 100644
--- a/public/admin/cactus-curator.html
+++ b/public/admin/cactus-curator.html
@@ -748,105 +748,112 @@ $('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.
+// ===== "By-base layout" — index page mode =================================
+// Swaps the flat grid for a vertical stack of FAMILY CARDS — each row is one
+// base pattern shown large, with a horizontal strip of all its colorway
+// children below. Works on ANY ?category= that has parent_design_id linkage
+// (face-skull-damask, cactus, chinoiserie, designer-zoo-calm, designer-scenic,
+// drunk-zoo-36, muybridge-plate, stripe, throttled-plaster-roses). Toggle
+// state persists in localStorage so it stays on across reloads.
(() => {
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;
+ const LS_KEY = 'cactus_curator_bblayout';
+ let families = [], stackEl = null, loaded = false, btn = null;
+
+ function setBtnState(on) {
+ if (!btn) return;
+ btn.textContent = (on ? '◆ ' : '🎨 ') + 'By-base layout';
+ btn.style.background = on ? 'var(--ink, #eef1f3)' : '';
+ btn.style.color = on ? '#111' : '';
+ btn.title = on ? 'showing 1 pattern + colorways per row — click to return to flat grid'
+ : 'group designs by base pattern with their colorway thumbs underneath (works on any collection)';
+ }
+
+ function ensureStack() {
+ if (stackEl) return stackEl;
+ stackEl = document.createElement('div');
+ stackEl.id = 'bbstack';
+ stackEl.style.cssText = 'display:none;width:100%;padding:8px 4px 80px;box-sizing:border-box;';
+ const grid = document.getElementById('grid');
+ if (grid && grid.parentNode) grid.parentNode.insertBefore(stackEl, grid.nextSibling);
+ else document.body.appendChild(stackEl);
+ return stackEl;
}
- function renderView() {
- if (!families.length) return;
- const fam = families[idx];
+ function familyCardHTML(fam) {
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(); });
+ const baseHref = `/admin/seam-debug/${fam.id}`;
+ return `<div class="bbfam" data-base="${fam.id}" style="background:var(--panel,#17191b);border:1px solid var(--line,#2a2e31);border-radius:12px;padding:12px;margin-bottom:14px">
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;font:12px ui-monospace,monospace;color:var(--mut,#9aa3ab)">
+ <span>base <b style="color:var(--ink,#eef1f3)">#${fam.id}</b> · ${fam.category} · ${all.length} colorways</span>
+ <span>${fam.is_published ? '<span style="color:#7bd28e">✓ published</span>' : '<span style="color:#aaa">○ unpublished</span>'}</span>
+ </div>
+ <div style="display:grid;grid-template-columns:minmax(0,1fr);gap:10px">
+ <a href="${baseHref}" target="_blank" style="display:block;background:#000;border-radius:8px;overflow:hidden">
+ <img src="/designs/img/by-id/${fam.id}" loading="lazy" style="display:block;width:100%;max-height:520px;object-fit:contain;background:#fff">
+ </a>
+ <div style="display:flex;gap:6px;overflow-x:auto;padding:6px 2px;scrollbar-width:thin">
+ ${all.map(c => {
+ const cw = (c.category || '').split(' · ')[1] || 'base';
+ const isBase = c.id === fam.id;
+ const border = isBase ? '#ffd68a' : (c.is_published ? '#3a6a3a' : '#3a3a3a');
+ return `<a href="/admin/seam-debug/${c.id}" target="_blank" class="bbthumb" data-id="${c.id}" data-fam="${fam.id}" style="flex:0 0 auto;border:2px solid ${border};border-radius:6px;overflow:hidden;background:#222;text-decoration:none;color:inherit">
+ <img src="/designs/img/by-id/${c.id}" loading="lazy" style="display:block;width:84px;height:84px;object-fit:cover">
+ <div style="text-align:center;font:700 9px sans-serif;padding:3px 4px;color:${isBase ? '#ffd68a' : '#aaa'};max-width:84px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${cw}</div>
+ </a>`;
+ }).join('')}
+ </div>
+ </div>
+ </div>`;
}
- function go(delta) {
- const n = idx + delta; if (n < 0 || n >= families.length) return;
- idx = n; mainId = null; renderView();
+ function renderStack() {
+ if (!stackEl) ensureStack();
+ if (!families.length) { stackEl.innerHTML = '<div style="text-align:center;padding:60px;color:var(--mut,#9aa3ab)">no bases with colorway children in this category</div>'; return; }
+ stackEl.innerHTML = families.map(familyCardHTML).join('');
}
- 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>';
+ function showStack() {
+ const grid = document.getElementById('grid'); if (grid) grid.style.display = 'none';
+ ensureStack().style.display = 'block';
+ if (!loaded) {
+ stackEl.innerHTML = '<div style="text-align:center;padding:60px;color:var(--mut,#9aa3ab)">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(); }
+ loaded = true;
+ renderStack();
})
- .catch(e => overlay.querySelector('#bbmain').innerHTML = '<div style="color:#f88">load failed: ' + (e.message || e) + '</div>');
- } else renderView();
+ .catch(e => { stackEl.innerHTML = '<div style="color:#f88;padding:20px;text-align:center">load failed: ' + (e.message || e) + '</div>'; });
+ }
+ }
+ function hideStack() {
+ const grid = document.getElementById('grid'); if (grid) grid.style.display = '';
+ if (stackEl) stackEl.style.display = 'none';
+ }
+ function setMode(on) {
+ if (on) { localStorage.setItem(LS_KEY, '1'); showStack(); }
+ else { localStorage.removeItem(LS_KEY); hideStack(); }
+ setBtnState(on);
}
- function closeView() { if (overlay) overlay.style.display = 'none'; }
-
- // inject toggle button into the controls bar (re-injects if removed)
+ // inject the toggle button into the controls bar (re-injects if DOM rebuilds)
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);
+ btn = document.createElement('button'); btn.id = 'bbtoggle';
+ btn.className = anchor.className || '';
+ btn.onclick = () => setMode(localStorage.getItem(LS_KEY) !== '1');
+ anchor.parentNode.insertBefore(btn, anchor.nextSibling);
+ setBtnState(localStorage.getItem(LS_KEY) === '1');
+ // restore mode on first paint if saved
+ if (localStorage.getItem(LS_KEY) === '1') setMode(true);
}).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>
← ae36473 etsy bundles: scripts/generate-etsy-bundles.js groups bucket
·
back to Wallco Ai
·
etsy push: scripts/push-etsy-bundle.js — atomic just-in-time 5ad0135 →