← back to Muralsource Curator
index.html
190 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Mural Source — Settlement Curator (228)</title>
<style>
:root{--cols:5;}
*{box-sizing:border-box}
body{margin:0;font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#0e0f12;color:#e7e7ea}
header{position:sticky;top:0;z-index:10;background:#15171c;border-bottom:1px solid #262a32;padding:10px 16px;display:flex;flex-wrap:wrap;gap:12px;align-items:center}
h1{font-size:15px;margin:0;font-weight:600;letter-spacing:.02em}
.muted{color:#8b90a0}
input,select,button{font:inherit;background:#1c1f26;color:#e7e7ea;border:1px solid #2c313b;border-radius:8px;padding:7px 10px}
button{cursor:pointer}
button.primary{background:#2f6df6;border-color:#2f6df6;color:#fff;font-weight:600}
button.ghost{background:transparent}
.controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.count{font-weight:700}
/* Grid / List toggle */
.vbtn{font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid #2c313b;background:#1c1f26;color:#8b90a0;cursor:pointer}
.vbtn.on{background:#2f6df6;border-color:#2f6df6;color:#fff}
/* Fields popover */
.fieldmenu{position:relative}
.fbtn{font-size:12px;padding:5px 10px;border-radius:6px;border:1px solid #2c313b;background:#1c1f26;color:#8b90a0;cursor:pointer}
.fbtn:hover{border-color:#5a9af0;color:#e7e7ea}
.fpop{position:absolute;top:calc(100% + 6px);right:0;background:#1c1f26;border:1px solid #2c313b;border-radius:8px;box-shadow:0 10px 28px rgba(0,0,0,.5);padding:8px 4px;z-index:30;min-width:170px}
.fpop[hidden]{display:none}
.fpop label{display:flex;align-items:center;gap:8px;padding:6px 12px;font-size:12px;color:#b8bec9;cursor:pointer;white-space:nowrap}
.fpop label:hover{background:#21252e;color:#e7e7ea}
.fpop input{accent-color:#2f6df6;width:14px;height:14px;margin:0;padding:0}
/* Grid view */
#grid{display:grid;grid-template-columns:repeat(var(--cols),1fr);gap:12px;padding:16px}
/* List view */
#grid.list{display:flex;flex-direction:column;gap:6px;padding:16px}
#grid.list .card{display:flex;flex-direction:row;align-items:stretch;border-radius:8px}
#grid.list .card .imgwrap{aspect-ratio:1/1;width:72px;min-width:72px;height:72px;border-radius:6px 0 0 6px}
#grid.list .card .meta{flex:1;display:flex;flex-direction:row;align-items:center;gap:16px;padding:8px 12px;flex-wrap:wrap}
#grid.list .card .price{position:static;border:0;background:none;font-weight:700;font-size:12px;white-space:nowrap}
#grid.list .card .chk{position:static;width:18px;height:18px}
#grid.list .card .sku{position:static;font-size:10px;color:#6f7688}
/* Field visibility — body class hides tagged elements */
body.hf-image [data-f="image"]{display:none!important}
body.hf-title [data-f="title"]{display:none!important}
body.hf-sub [data-f="sub"]{display:none!important}
body.hf-tags [data-f="tags"]{display:none!important}
body.hf-price [data-f="price"]{display:none!important}
body.hf-sku [data-f="sku"]{display:none!important}
body.hf-when [data-f="when"]{display:none!important}
.card{background:#15171c;border:1px solid #262a32;border-radius:12px;overflow:hidden;position:relative;transition:border-color .15s}
.card.sel{border-color:#2f6df6;box-shadow:0 0 0 2px #2f6df6 inset}
.card .imgwrap{aspect-ratio:1/1.1;background:#0a0b0d;overflow:hidden;display:flex;align-items:center;justify-content:center}
.card img{width:100%;height:100%;object-fit:cover;display:block}
.card .meta{padding:8px 10px}
.card .pat{font-weight:600;font-size:13px;line-height:1.25}
.card .sub{color:#9aa0b0;font-size:11px;margin-top:2px}
.card .tags{margin-top:6px;font-size:10px;color:#c9b98a;line-height:1.35;max-height:2.7em;overflow:hidden}
.card .when{margin-top:6px;font-size:10px;color:#6f7688}
.card .price{position:absolute;top:8px;right:8px;background:#0e0f12cc;border:1px solid #2c313b;border-radius:6px;padding:2px 6px;font-size:11px;font-weight:700}
.card .chk{position:absolute;top:8px;left:8px;width:22px;height:22px;accent-color:#2f6df6;cursor:pointer}
.card .sku{position:absolute;bottom:8px;left:10px;font-size:10px;color:#6f7688;font-family:ui-monospace,monospace}
#export{position:fixed;bottom:0;left:0;right:0;background:#15171c;border-top:1px solid #262a32;padding:10px 16px;display:none;gap:10px;align-items:center}
#export.show{display:flex}
textarea{width:100%;height:70px;background:#0a0b0d;color:#9fe6a0;border:1px solid #2c313b;border-radius:8px;font-family:ui-monospace,monospace;font-size:11px;padding:8px}
a{color:#6ea8ff}
</style>
</head>
<body>
<header>
<h1>Mural Source — Settlement Curator</h1>
<span class="muted">Check the murals that <b>PASS</b> the settlement (no bananas/grapes/birds/butterflies + directional foliage w/ open space). <span class="count" id="selN">0</span> selected / <span id="totN">0</span></span>
<div class="controls" style="margin-left:auto">
<input id="q" placeholder="filter pattern / color / tag" size="22">
<select id="sort">
<option value="pattern">Pattern A→Z</option>
<option value="color">Color</option>
<option value="price_asc">Price ↑</option>
<option value="price_desc">Price ↓</option>
<option value="sku">SKU</option>
</select>
<label class="muted">density <input id="dens" type="range" min="3" max="8" value="5"></label>
<button class="vbtn on" id="vGrid" title="Grid view">⊞ Grid</button>
<button class="vbtn" id="vList" title="List view">☰ List</button>
<div class="fieldmenu">
<button class="fbtn" id="fieldsBtn" aria-haspopup="true" aria-expanded="false">Fields ▾</button>
<div class="fpop" id="fpop" hidden role="menu"></div>
</div>
<button class="ghost" id="selAll">select all shown</button>
<button class="ghost" id="selNone">clear</button>
<button class="primary" id="exp">Export selection ▸</button>
</div>
</header>
<div id="grid"></div>
<div id="export">
<div style="flex:1">
<div class="muted" style="margin-bottom:4px">Selected DW SKUs — copy this back to Claude to push (settlement-PASS set):</div>
<textarea id="out" readonly></textarea>
</div>
<button class="primary" id="copy">Copy</button>
<button class="ghost" id="close">×</button>
</div>
<script>
const LS='ms_curator_sel', LSD='ms_curator_dens', LSS='ms_curator_sort',
LSV='ms_curator_view', LSF='ms_curator_fields';
let items=[], sel=new Set(JSON.parse(localStorage.getItem(LS)||'[]'));
const grid=document.getElementById('grid'), q=document.getElementById('q'),
sortEl=document.getElementById('sort'), dens=document.getElementById('dens');
dens.value=localStorage.getItem(LSD)||5; document.documentElement.style.setProperty('--cols',dens.value);
sortEl.value=localStorage.getItem(LSS)||'pattern';
// ── Grid / List toggle ──
const MS_FIELDS=[['image','Image'],['title','Title'],['sub','Color / Type'],['tags','Tags'],['price','Price'],['sku','SKU'],['when','Date / Link']];
const MS_FIELDS_DEF=Object.fromEntries(MS_FIELDS.map(([k])=>[k,true]));
let msFieldVis=Object.assign({},MS_FIELDS_DEF,JSON.parse(localStorage.getItem(LSF)||'{}'));
function applyMsFields(){
MS_FIELDS.forEach(([k])=>document.body.classList.toggle('hf-'+k,!msFieldVis[k]));
document.querySelectorAll('#fpop input[data-field]').forEach(cb=>cb.checked=!!msFieldVis[cb.dataset.field]);
localStorage.setItem(LSF,JSON.stringify(msFieldVis));
}
function buildMsFieldMenu(){
const fpop=document.getElementById('fpop');
fpop.innerHTML=MS_FIELDS.map(([k,lbl])=>`<label><input type="checkbox" data-field="${k}"${msFieldVis[k]?' checked':''}> ${lbl}</label>`).join('');
fpop.querySelectorAll('input').forEach(cb=>cb.addEventListener('change',()=>{msFieldVis[cb.dataset.field]=cb.checked;localStorage.setItem(LSF,JSON.stringify(msFieldVis));applyMsFields();}));
}
function setMsView(v){
grid.classList.toggle('list',v==='list');
document.getElementById('vGrid').classList.toggle('on',v==='grid');
document.getElementById('vList').classList.toggle('on',v==='list');
localStorage.setItem(LSV,v);
}
document.getElementById('vGrid').onclick=()=>setMsView('grid');
document.getElementById('vList').onclick=()=>setMsView('list');
setMsView(localStorage.getItem(LSV)||'grid');
buildMsFieldMenu();
applyMsFields();
// Fields popover open/close
(function(){
const btn=document.getElementById('fieldsBtn'), pop=document.getElementById('fpop');
btn.addEventListener('click',e=>{e.stopPropagation();const h=pop.hidden;pop.hidden=!h;btn.setAttribute('aria-expanded',String(h));});
document.addEventListener('click',()=>{pop.hidden=true;btn.setAttribute('aria-expanded','false');});
pop.addEventListener('click',e=>e.stopPropagation());
})();
function tagList(s){try{return JSON.parse(s)}catch{return[]}}
function render(){
const term=q.value.trim().toLowerCase();
let list=items.filter(it=>!term|| (it.pattern_name+' '+it.color_name+' '+it.ai_tags).toLowerCase().includes(term));
const s=sortEl.value;
list.sort((a,b)=> s==='price_asc'? a.price_retail-b.price_retail :
s==='price_desc'? b.price_retail-a.price_retail :
s==='color'? (a.color_name||'').localeCompare(b.color_name||'') :
s==='sku'? a.dw_sku.localeCompare(b.dw_sku) :
(a.pattern_name||'').localeCompare(b.pattern_name||''));
grid.innerHTML='';
for(const it of list){
const c=document.createElement('div'); c.className='card'+(sel.has(it.dw_sku)?' sel':'');
const tags=tagList(it.ai_tags).slice(0,6).join(' · ');
c.innerHTML=`<div class="imgwrap" data-f="image"><img loading="lazy" src="${it.image_url}" alt="${it.pattern_name}"
onerror="this.style.opacity=.2;this.alt='no image'"></div>
<input type="checkbox" class="chk" ${sel.has(it.dw_sku)?'checked':''}>
<span class="price" data-f="price">$${Number(it.price_retail).toFixed(0)}</span>
<span class="sku" data-f="sku">${it.dw_sku}</span>
<div class="meta"><div class="pat" data-f="title">${it.pattern_name}</div>
<div class="sub" data-f="sub">${it.color_name||''} · ${it.product_type||''}</div>
<div class="tags" data-f="tags">${tags}</div>
<div class="when" data-f="when" title="scraped ${it.refreshed_at}">🕓 ${it.created_at} · <a href="${it.product_url}" target="_blank" rel="noopener noreferrer">vendor ↗</a></div></div>`;
const toggle=()=>{ sel.has(it.dw_sku)?sel.delete(it.dw_sku):sel.add(it.dw_sku);
c.classList.toggle('sel'); c.querySelector('.chk').checked=sel.has(it.dw_sku); save(); };
c.querySelector('.chk').addEventListener('click',e=>{e.stopPropagation();toggle();});
c.querySelector('.imgwrap').addEventListener('click',toggle);
grid.appendChild(c);
}
document.getElementById('totN').textContent=items.length;
document.getElementById('selN').textContent=sel.size;
}
function save(){localStorage.setItem(LS,JSON.stringify([...sel]));document.getElementById('selN').textContent=sel.size;}
q.oninput=render;
sortEl.onchange=()=>{localStorage.setItem(LSS,sortEl.value);render();};
dens.oninput=()=>{document.documentElement.style.setProperty('--cols',dens.value);localStorage.setItem(LSD,dens.value);};
document.getElementById('selAll').onclick=()=>{document.querySelectorAll('#grid .card').forEach((c,i)=>{});
const term=q.value.trim().toLowerCase();
items.filter(it=>!term||(it.pattern_name+' '+it.color_name+' '+it.ai_tags).toLowerCase().includes(term)).forEach(it=>sel.add(it.dw_sku));save();render();};
document.getElementById('selNone').onclick=()=>{sel.clear();save();render();};
document.getElementById('exp').onclick=()=>{
document.getElementById('out').value=[...sel].sort().join(',');
document.getElementById('export').classList.add('show');};
document.getElementById('close').onclick=()=>document.getElementById('export').classList.remove('show');
document.getElementById('copy').onclick=()=>{const t=document.getElementById('out');t.select();document.execCommand('copy');};
fetch('items.json').then(r=>r.json()).then(d=>{items=d;render();});
</script>
</body></html>