← back to Wallco Ai
feat(styleguides): Brand Selector admin tool wired to generator + studio recolor
0fbb86a497306b64073a1f47bf2825c3b0eec932 · 2026-06-01 11:11:28 -0700 · Steve Abrams
- public/admin/styleguides.html: faithful dark-luxe rebuild of the Paper
'Brand Selector' artboard — 20 fashion + 10 wallcovering houses, selectable
cards w/ country/est chips + palette dots, select-all/clear/top-5 controls,
selection footer.
- 3 live actions: Generate (POST /api/generator/batch seeded by selected
houses), Apply to Studio (POST /api/design/:id/recolor), Export JSON/Figma.
- scripts/fashion_palettes.js: +country/est/cat; +8 fashion + 10 wallcovering
houses so all 30 cards drive real generation/recolor.
- server.js: GET /admin/styleguides (admin), GET /api/styleguides/brands,
host-aware '/' so the styleguides.wallco.ai vhost root serves the gated page.
Files touched
A public/admin/styleguides.htmlM scripts/fashion_palettes.jsM server.js
Diff
commit 0fbb86a497306b64073a1f47bf2825c3b0eec932
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 1 11:11:28 2026 -0700
feat(styleguides): Brand Selector admin tool wired to generator + studio recolor
- public/admin/styleguides.html: faithful dark-luxe rebuild of the Paper
'Brand Selector' artboard — 20 fashion + 10 wallcovering houses, selectable
cards w/ country/est chips + palette dots, select-all/clear/top-5 controls,
selection footer.
- 3 live actions: Generate (POST /api/generator/batch seeded by selected
houses), Apply to Studio (POST /api/design/:id/recolor), Export JSON/Figma.
- scripts/fashion_palettes.js: +country/est/cat; +8 fashion + 10 wallcovering
houses so all 30 cards drive real generation/recolor.
- server.js: GET /admin/styleguides (admin), GET /api/styleguides/brands,
host-aware '/' so the styleguides.wallco.ai vhost root serves the gated page.
---
public/admin/styleguides.html | 393 ++++++++++++++++++++++++++++++++++++++++++
scripts/fashion_palettes.js | 61 +++++--
server.js | 38 ++++
3 files changed, 473 insertions(+), 19 deletions(-)
diff --git a/public/admin/styleguides.html b/public/admin/styleguides.html
new file mode 100644
index 0000000..c71ff57
--- /dev/null
+++ b/public/admin/styleguides.html
@@ -0,0 +1,393 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>Brand Selector · Style Guides — wallco.ai</title>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;0,600;1,400;1,500&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
+<style>
+ :root{
+ --bg:#0a0a0e; --bg2:#0d0d12; --card:#121218; --card-hi:#16161d;
+ --line:#26262f; --line-soft:#1d1d25;
+ --ink:#F4F4F5; --ink-soft:#b6b6c0; --muted:#8a8a96; --faint:#6a6a76;
+ --gold:#E4C25A; --gold-soft:#caa84a;
+ --serif:"Cormorant Garamond","Didot","Times New Roman",serif;
+ --sans:"Inter",system-ui,-apple-system,sans-serif;
+ }
+ *{box-sizing:border-box;margin:0;padding:0}
+ html,body{background:var(--bg);color:var(--ink)}
+ body{font:15px/1.5 var(--sans);-webkit-font-smoothing:antialiased;padding-bottom:96px}
+ a{color:inherit}
+ .wrap{max-width:1312px;margin:0 auto;padding:0 28px}
+
+ /* ── Header ── */
+ header{border-bottom:1px solid var(--line-soft);background:linear-gradient(180deg,#0c0c11,#0a0a0e);padding:42px 0 30px}
+ .eyebrow{display:flex;align-items:center;gap:9px;font:600 11px var(--sans);letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin-bottom:14px}
+ .eyebrow .dot{width:7px;height:7px;border-radius:50%;background:var(--gold);box-shadow:0 0 10px var(--gold)}
+ .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap}
+ h1{font:500 64px/0.95 var(--serif);letter-spacing:-.01em}
+ .lede{font:italic 400 18px/1.4 var(--serif);color:var(--ink-soft);margin-top:12px;max-width:680px}
+ .counts{display:flex;gap:40px}
+ .counts .c .n{font:500 46px/1 var(--serif);color:var(--gold)}
+ .counts .c .l{font:600 10.5px var(--sans);letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin-top:6px}
+
+ /* ── Section ── */
+ section{padding:44px 0 8px}
+ .sec-top{display:flex;justify-content:space-between;align-items:flex-end;gap:18px;flex-wrap:wrap;margin-bottom:26px}
+ .sec-no{font:600 11px var(--sans);letter-spacing:.24em;color:var(--faint);margin-bottom:8px}
+ .sec-h{font:500 38px/1 var(--serif)}
+ .sec-sub{font:italic 400 14.5px var(--serif);color:var(--muted);margin-top:6px}
+ .controls{display:flex;gap:10px;align-items:center}
+ .ctl{font:500 12px var(--sans);color:var(--ink-soft);background:transparent;border:1px solid var(--line);border-radius:999px;padding:8px 16px;cursor:pointer;transition:.15s}
+ .ctl:hover{border-color:var(--muted);color:var(--ink)}
+ .ctl.gold{border-color:var(--gold-soft);color:var(--gold)}
+ .ctl.gold:hover{background:rgba(228,194,90,.08)}
+
+ /* ── Grid ── */
+ .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:18px}
+ @media(max-width:1100px){.grid{grid-template-columns:repeat(2,1fr)}}
+ @media(max-width:640px){.grid{grid-template-columns:1fr}}
+ .card{position:relative;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px 22px 20px;min-height:150px;cursor:pointer;transition:.16s;user-select:none}
+ .card:hover{background:var(--card-hi);border-color:#34343f;transform:translateY(-1px)}
+ .card.sel{border-color:var(--gold);background:#14130f;box-shadow:0 0 0 1px var(--gold),0 8px 30px rgba(228,194,90,.07)}
+ .card .name{font:400 21px/1.1 var(--serif);color:var(--ink);padding-right:34px}
+ .card .meta{display:flex;gap:6px;margin-top:13px}
+ .chip{border:1px solid var(--line);border-radius:999px;padding:3px 9px;font:500 9.5px var(--sans);letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
+ .dots{display:flex;gap:6px;margin-top:16px}
+ .dot{width:17px;height:17px;border-radius:5px;border:1px solid rgba(255,255,255,.08)}
+ .tick{position:absolute;top:16px;right:16px;width:22px;height:22px;border-radius:50%;border:1.5px solid var(--line);display:flex;align-items:center;justify-content:center;transition:.15s}
+ .card.sel .tick{background:var(--gold);border-color:var(--gold)}
+ .tick svg{opacity:0;transition:.15s;width:12px;height:12px}
+ .card.sel .tick svg{opacity:1}
+
+ /* ── Footer bar ── */
+ .bar{position:fixed;left:0;right:0;bottom:0;z-index:40;background:rgba(10,10,14,.92);backdrop-filter:blur(14px);border-top:1px solid var(--line)}
+ .bar-in{max-width:1312px;margin:0 auto;padding:14px 28px;display:flex;align-items:center;gap:16px}
+ .selpill{font:600 11.5px var(--sans);letter-spacing:.04em;color:var(--ink);background:#1b1b22;border:1px solid var(--line);border-radius:999px;padding:8px 15px;white-space:nowrap}
+ .selnames{font:13px var(--sans);color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
+ .bar .actions{display:flex;gap:10px;align-items:center;margin-left:auto}
+ .btn{font:600 12.5px var(--sans);border-radius:999px;padding:11px 20px;cursor:pointer;border:1px solid var(--line);background:transparent;color:var(--ink-soft);transition:.15s;white-space:nowrap}
+ .btn:hover{border-color:var(--muted);color:var(--ink)}
+ .btn.primary{background:var(--gold);color:#1a1505;border-color:var(--gold);font-weight:600}
+ .btn.primary:hover{background:#eccd6e}
+ .btn:disabled{opacity:.4;cursor:not-allowed}
+
+ /* ── Modal ── */
+ .scrim{position:fixed;inset:0;z-index:60;background:rgba(0,0,0,.66);display:none;align-items:center;justify-content:center;padding:32px}
+ .scrim.on{display:flex}
+ .modal{background:var(--bg2);border:1px solid var(--line);border-radius:16px;max-width:780px;width:100%;max-height:84vh;overflow:auto;padding:28px}
+ .modal h3{font:500 26px var(--serif);margin-bottom:4px}
+ .modal .mh-sub{color:var(--muted);font-size:13px;margin-bottom:18px}
+ pre{background:#08080b;border:1px solid var(--line);border-radius:10px;padding:18px;font:12.5px/1.6 ui-monospace,Menlo,monospace;color:#cfcfd8;overflow:auto;white-space:pre-wrap;word-break:break-word}
+ .modal-row{display:flex;gap:10px;margin-top:16px;flex-wrap:wrap}
+ .field{display:flex;align-items:center;gap:8px}
+ .field label{font:600 11px var(--sans);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
+ .field input[type=number]{width:74px;background:#08080b;border:1px solid var(--line);border-radius:8px;color:var(--ink);padding:9px 11px;font:14px var(--sans)}
+ .status{font:13px var(--sans);color:var(--ink-soft);margin-top:14px;min-height:20px}
+ .picker{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:16px}
+ .picker img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;border:1px solid var(--line);cursor:pointer;transition:.15s}
+ .picker .pi{position:relative}
+ .picker img:hover{border-color:var(--gold);transform:scale(1.02)}
+ .brandsel{display:flex;flex-wrap:wrap;gap:7px;margin:14px 0}
+ .brandsel button{font:500 12px var(--sans);border:1px solid var(--line);background:#101016;color:var(--ink-soft);border-radius:999px;padding:7px 13px;cursor:pointer}
+ .brandsel button.on{border-color:var(--gold);color:var(--gold);background:rgba(228,194,90,.1)}
+ .results{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:14px}
+ .results .r{text-align:center}
+ .results img{width:100%;aspect-ratio:1;object-fit:cover;border-radius:9px;border:1px solid var(--line)}
+ .results .cap{font:10px var(--sans);color:var(--muted);margin-top:5px}
+ .x{float:right;font-size:22px;color:var(--muted);cursor:pointer;line-height:1;margin-top:-4px}
+ .toast{position:fixed;left:50%;bottom:108px;transform:translateX(-50%);z-index:80;background:#16161d;border:1px solid var(--gold-soft);color:var(--ink);font:500 13px var(--sans);padding:12px 20px;border-radius:10px;opacity:0;transition:.25s;pointer-events:none}
+ .toast.on{opacity:1}
+ .skeleton{color:var(--faint);font:14px var(--sans);padding:40px 0;text-align:center}
+</style>
+</head>
+<body>
+<header>
+ <div class="wrap">
+ <div class="eyebrow"><span class="dot"></span> Style Guides · wallco.ai · V1</div>
+ <div class="head-row">
+ <div>
+ <h1>Brand Selector</h1>
+ <div class="lede">Pick the houses that source the next generative build · Top 20 fashion · Top 10 wallcovering</div>
+ </div>
+ <div class="counts">
+ <div class="c"><div class="n" id="cnt-fashion">20</div><div class="l">Fashion</div></div>
+ <div class="c"><div class="n" id="cnt-wall">10</div><div class="l">Wallcovering</div></div>
+ </div>
+ </div>
+ </div>
+</header>
+
+<main class="wrap">
+ <section>
+ <div class="sec-top">
+ <div>
+ <div class="sec-no">I.</div>
+ <div class="sec-h">Fashion</div>
+ <div class="sec-sub">Top 20 — Luxury & Iconic Houses</div>
+ </div>
+ <div class="controls">
+ <button class="ctl" data-all="fashion">Select all 20</button>
+ <button class="ctl" data-clear="fashion">Clear</button>
+ <button class="ctl gold" data-top5="fashion">Top 5 only</button>
+ </div>
+ </div>
+ <div class="grid" id="grid-fashion"><div class="skeleton">Loading houses…</div></div>
+ </section>
+
+ <section>
+ <div class="sec-top">
+ <div>
+ <div class="sec-no">II.</div>
+ <div class="sec-h">Wallcovering</div>
+ <div class="sec-sub">Top 10 — Heritage & Designer Houses</div>
+ </div>
+ <div class="controls">
+ <button class="ctl" data-all="wallcovering">Select all 10</button>
+ <button class="ctl" data-clear="wallcovering">Clear</button>
+ </div>
+ </div>
+ <div class="grid" id="grid-wallcovering"></div>
+ </section>
+</main>
+
+<div class="bar">
+ <div class="bar-in">
+ <span class="selpill"><b id="sel-count">0</b> selected</span>
+ <span class="selnames" id="sel-names">No houses selected</span>
+ <div class="actions">
+ <button class="btn" id="btn-clear">Clear all</button>
+ <button class="btn" id="btn-json">Preview JSON</button>
+ <button class="btn" id="btn-export">Export</button>
+ <button class="btn" id="btn-apply">Apply to Studio →</button>
+ <button class="btn primary" id="btn-generate">Generate Designs →</button>
+ </div>
+ </div>
+</div>
+
+<!-- JSON modal -->
+<div class="scrim" id="scrim-json">
+ <div class="modal">
+ <span class="x" data-close>×</span>
+ <h3>Style-Guide Payload</h3>
+ <div class="mh-sub">Portable JSON for the generator, Figma build, or studio recolor.</div>
+ <pre id="json-out"></pre>
+ <div class="modal-row">
+ <button class="btn" id="btn-copy">Copy JSON</button>
+ <button class="btn" id="btn-copy-figma">Copy for Figma</button>
+ <button class="btn" id="btn-dl">Download .json</button>
+ </div>
+ </div>
+</div>
+
+<!-- Generate modal -->
+<div class="scrim" id="scrim-gen">
+ <div class="modal">
+ <span class="x" data-close>×</span>
+ <h3>Generate Designs</h3>
+ <div class="mh-sub">Renders new seamless tiles seeded from the selected houses' palettes & signatures. Each render passes the settlement + seam gates.</div>
+ <pre id="gen-prompt"></pre>
+ <div class="modal-row">
+ <div class="field"><label>Count</label><input type="number" id="gen-n" value="4" min="1" max="50"></div>
+ <button class="btn primary" id="btn-gen-go">Render batch →</button>
+ <a class="btn" href="/designs" target="_blank" rel="noopener noreferrer">Open Studio ↗</a>
+ </div>
+ <div class="status" id="gen-status"></div>
+ </div>
+</div>
+
+<!-- Apply to Studio modal -->
+<div class="scrim" id="scrim-apply">
+ <div class="modal">
+ <span class="x" data-close>×</span>
+ <h3>Apply to Studio</h3>
+ <div class="mh-sub">Recolor an existing design into a selected house palette. Pick a house, then click a design.</div>
+ <div class="brandsel" id="apply-brands"></div>
+ <div class="picker" id="apply-picker"><div class="skeleton">Loading recent designs…</div></div>
+ <div class="status" id="apply-status"></div>
+ <div class="results" id="apply-results"></div>
+ </div>
+</div>
+
+<div class="toast" id="toast"></div>
+
+<script>
+(function(){
+ // ── admin-token passthrough (page may be opened with ?admin=<token>) ──────
+ const QS = new URLSearchParams(location.search);
+ const ADMIN = QS.get('admin') || '';
+ const authd = (p) => ADMIN ? p + (p.includes('?') ? '&' : '?') + 'admin=' + encodeURIComponent(ADMIN) : p;
+ const jfetch = (p, opts) => fetch(authd(p), Object.assign({ credentials:'same-origin' }, opts || {}));
+
+ const state = { brands: { fashion: [], wallcovering: [] }, sel: new Set() };
+ const byKey = {};
+
+ const $ = (s, r=document) => r.querySelector(s);
+ const el = (t, c) => { const e=document.createElement(t); if(c)e.className=c; return e; };
+ let toastT;
+ function toast(msg){ const t=$('#toast'); t.textContent=msg; t.classList.add('on'); clearTimeout(toastT); toastT=setTimeout(()=>t.classList.remove('on'),2600); }
+
+ // ── render cards ──────────────────────────────────────────────────────────
+ function cardFor(b){
+ const c = el('div','card'); c.dataset.key=b.key;
+ const dots = b.hex.slice(0,3).map(h=>`<span class="dot" style="background:${h}"></span>`).join('');
+ c.innerHTML =
+ `<div class="tick"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12l4.5 4.5L19 7" stroke="#1a1505" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg></div>`+
+ `<div class="name">${b.label}</div>`+
+ `<div class="meta"><span class="chip">${b.country||'—'}</span><span class="chip">est. ${b.est||'—'}</span></div>`+
+ `<div class="dots">${dots}</div>`;
+ c.addEventListener('click',()=>toggle(b.key,c));
+ return c;
+ }
+ function renderGrids(){
+ const gf=$('#grid-fashion'), gw=$('#grid-wallcovering');
+ gf.innerHTML=''; gw.innerHTML='';
+ state.brands.fashion.forEach(b=>gf.appendChild(cardFor(b)));
+ state.brands.wallcovering.forEach(b=>gw.appendChild(cardFor(b)));
+ $('#cnt-fashion').textContent=state.brands.fashion.length;
+ $('#cnt-wall').textContent=state.brands.wallcovering.length;
+ }
+ function cardEl(key){ return document.querySelector(`.card[data-key="${key}"]`); }
+
+ function toggle(key,c){ c=c||cardEl(key); if(!c) return;
+ if(state.sel.has(key)){state.sel.delete(key);c.classList.remove('sel');}
+ else{state.sel.add(key);c.classList.add('sel');}
+ syncBar();
+ }
+ function setSel(key,on){ const c=cardEl(key); if(!c)return; if(on){state.sel.add(key);c.classList.add('sel');}else{state.sel.delete(key);c.classList.remove('sel');} }
+
+ function syncBar(){
+ const n=state.sel.size;
+ $('#sel-count').textContent=n;
+ const names=[...state.sel].map(k=>byKey[k]&&byKey[k].label).filter(Boolean);
+ $('#sel-names').textContent = n? names.join(' · ') : 'No houses selected';
+ ['btn-json','btn-export','btn-apply','btn-generate'].forEach(id=>$('#'+id).disabled = n===0);
+ }
+
+ // ── payload ───────────────────────────────────────────────────────────────
+ function selected(){ return [...state.sel].map(k=>byKey[k]).filter(Boolean); }
+ function payload(){
+ const houses=selected();
+ const colors=[]; houses.forEach(h=>h.hex.slice(0,2).forEach(x=>{ if(!colors.includes(x)) colors.push(x); }));
+ return {
+ version:'wallco-styleguide-v1',
+ generated_at:new Date().toISOString(),
+ count:houses.length,
+ houses:houses.map(h=>({key:h.key,label:h.label,category:h.cat,country:h.country,est:h.est,hex:h.hex,signature:h.signature})),
+ combined_palette:colors,
+ signatures:houses.map(h=>h.signature)
+ };
+ }
+ function genPrompt(){
+ const houses=selected();
+ const colors=[]; houses.forEach(h=>h.hex.slice(0,2).forEach(x=>{ if(!colors.includes(x)) colors.push(x); }));
+ const sigs=houses.map(h=>h.signature).join('; ');
+ return {
+ n: parseInt($('#gen-n').value,10)||4,
+ colors: colors.slice(0,6).join(', '),
+ patterns: houses.map(h=>({artist:h.label, title:h.label+' signature', dominant_hex:h.hex[0]})),
+ styles: [],
+ extra_text: 'Drawing on the house signatures of '+houses.map(h=>h.label).join(', ')+': '+sigs+'. Tone-on-tone, archival, no more than 4 colors.'
+ };
+ }
+
+ // ── controls ────────────────────────────────────────────────────────────
+ document.querySelectorAll('[data-all]').forEach(b=>b.addEventListener('click',()=>{ state.brands[b.dataset.all].forEach(x=>setSel(x.key,true)); syncBar(); }));
+ document.querySelectorAll('[data-clear]').forEach(b=>b.addEventListener('click',()=>{ state.brands[b.dataset.clear].forEach(x=>setSel(x.key,false)); syncBar(); }));
+ document.querySelectorAll('[data-top5]').forEach(b=>b.addEventListener('click',()=>{ const cat=b.dataset.top5; state.brands[cat].forEach(x=>setSel(x.key,false)); state.brands[cat].slice(0,5).forEach(x=>setSel(x.key,true)); syncBar(); }));
+ $('#btn-clear').addEventListener('click',()=>{ state.sel.forEach(k=>{const c=cardEl(k);if(c)c.classList.remove('sel');}); state.sel.clear(); syncBar(); });
+
+ // ── modals ──────────────────────────────────────────────────────────────
+ function openModal(id){ $('#'+id).classList.add('on'); }
+ function closeModal(s){ s.classList.remove('on'); }
+ document.querySelectorAll('[data-close]').forEach(x=>x.addEventListener('click',e=>closeModal(e.target.closest('.scrim'))));
+ document.querySelectorAll('.scrim').forEach(s=>s.addEventListener('click',e=>{ if(e.target===s) closeModal(s); }));
+ document.addEventListener('keydown',e=>{ if(e.key==='Escape') document.querySelectorAll('.scrim.on').forEach(closeModal); });
+
+ // Preview JSON
+ $('#btn-json').addEventListener('click',()=>{ $('#json-out').textContent=JSON.stringify(payload(),null,2); openModal('scrim-json'); });
+ $('#btn-copy').addEventListener('click',()=>{ navigator.clipboard.writeText(JSON.stringify(payload(),null,2)); toast('JSON copied'); });
+ $('#btn-copy-figma').addEventListener('click',()=>{
+ const p=payload();
+ const txt='wallco.ai style guide — '+p.count+' houses\n'+p.houses.map(h=>`${h.label} (${h.country} · est. ${h.est}) — ${h.hex.join(' ')} — ${h.signature}`).join('\n');
+ navigator.clipboard.writeText(txt); toast('Figma block copied');
+ });
+ $('#btn-export').addEventListener('click',()=>dlJson());
+ $('#btn-dl').addEventListener('click',()=>dlJson());
+ function dlJson(){
+ const blob=new Blob([JSON.stringify(payload(),null,2)],{type:'application/json'});
+ const a=el('a'); a.href=URL.createObjectURL(blob); a.download='wallco-styleguide-'+Date.now()+'.json'; a.click();
+ setTimeout(()=>URL.revokeObjectURL(a.href),2000); toast('Downloaded');
+ }
+
+ // Generate
+ $('#btn-generate').addEventListener('click',()=>{
+ const p=genPrompt();
+ $('#gen-prompt').textContent='palette: '+(p.colors||'(none)')+'\nlineage: '+p.patterns.map(x=>x.artist).join(', ')+'\n\n'+p.extra_text;
+ $('#gen-status').textContent=''; openModal('scrim-gen');
+ });
+ $('#btn-gen-go').addEventListener('click',async()=>{
+ const go=$('#btn-gen-go'); go.disabled=true;
+ const p=genPrompt(); p.n=parseInt($('#gen-n').value,10)||4;
+ $('#gen-status').textContent='Kicking off render of '+p.n+' design'+(p.n>1?'s':'')+'…';
+ try{
+ const r=await jfetch('/api/generator/batch',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});
+ const j=await r.json();
+ if(!j.ok) throw new Error(j.error||'failed');
+ $('#gen-status').innerHTML='✓ Batch <b>#'+j.run_id+'</b> running — '+(j.note||'')+' <a href="/designs" target="_blank" rel="noopener noreferrer" style="color:var(--gold)">watch the Studio ↗</a>';
+ toast('Batch #'+j.run_id+' started');
+ }catch(e){ $('#gen-status').textContent='✗ '+e.message; }
+ finally{ go.disabled=false; }
+ });
+
+ // Apply to Studio
+ let applyBrand=null;
+ $('#btn-apply').addEventListener('click',async()=>{
+ const houses=selected();
+ const bs=$('#apply-brands'); bs.innerHTML='';
+ applyBrand=houses[0]?houses[0].key:null;
+ houses.forEach(h=>{ const b=el('button'); b.textContent=h.label; b.dataset.key=h.key; if(h.key===applyBrand)b.classList.add('on');
+ b.addEventListener('click',()=>{ applyBrand=h.key; bs.querySelectorAll('button').forEach(x=>x.classList.toggle('on',x.dataset.key===h.key)); });
+ bs.appendChild(b); });
+ $('#apply-results').innerHTML=''; $('#apply-status').textContent='';
+ openModal('scrim-apply');
+ const pk=$('#apply-picker'); pk.innerHTML='<div class="skeleton">Loading recent designs…</div>';
+ try{
+ const r=await jfetch('/api/designs/random?n=12'); const j=await r.json();
+ const items=(j.items||j||[]); pk.innerHTML='';
+ items.forEach(d=>{ const div=el('div','pi'); const img=el('img'); img.src=d.image_url; img.title='#'+d.id+(d.title?' · '+d.title:''); img.loading='lazy';
+ img.addEventListener('click',()=>recolor(d.id)); div.appendChild(img); pk.appendChild(div); });
+ if(!items.length) pk.innerHTML='<div class="skeleton">No designs available.</div>';
+ }catch(e){ pk.innerHTML='<div class="skeleton">Failed to load: '+e.message+'</div>'; }
+ });
+ async function recolor(id){
+ if(!applyBrand){ toast('Pick a house first'); return; }
+ const label=byKey[applyBrand].label;
+ $('#apply-status').textContent='→ Recoloring #'+id+' into '+label+'…';
+ try{
+ const r=await jfetch('/api/design/'+id+'/recolor',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({brand:applyBrand})});
+ const j=await r.json();
+ if(!j.ok) throw new Error(j.error||'failed');
+ const div=el('div','r');
+ div.innerHTML='<a href="/design/'+j.new_id+'" target="_blank" rel="noopener noreferrer"><img src="'+j.new_image_url+'"></a><div class="cap">'+label+' · #'+j.new_id+'</div>';
+ $('#apply-results').prepend(div);
+ $('#apply-status').textContent='✓ #'+id+' → '+label+' (#'+j.new_id+')';
+ toast('Recolored → #'+j.new_id);
+ }catch(e){ $('#apply-status').textContent='✗ '+e.message; }
+ }
+
+ // ── boot ──────────────────────────────────────────────────────────────────
+ (async function(){
+ try{
+ const r=await jfetch('/api/styleguides/brands'); const j=await r.json();
+ state.brands.fashion=j.fashion||[]; state.brands.wallcovering=j.wallcovering||[];
+ [...state.brands.fashion,...state.brands.wallcovering].forEach(b=>byKey[b.key]=b);
+ renderGrids(); syncBar();
+ }catch(e){ $('#grid-fashion').innerHTML='<div class="skeleton">Failed to load houses: '+e.message+'</div>'; }
+ })();
+})();
+</script>
+</body>
+</html>
diff --git a/scripts/fashion_palettes.js b/scripts/fashion_palettes.js
index c65fdd0..9639458 100644
--- a/scripts/fashion_palettes.js
+++ b/scripts/fashion_palettes.js
@@ -22,25 +22,48 @@ const path = require('path');
// categories pick only from the tone-on-tone subset; figure/scenic categories
// (drunk-animals, murals) may use any palette since they skip the seam check.
const FASHION_PALETTES = [
- { brand: 'Hermès', hex: ['#FF4F00', '#1a1410', '#FFFEFA', '#E5DDD0'], signature: 'burnt-orange accent, saddle-stitch warmth, generous ground', toneOnTone: false },
- { brand: 'Bottega Veneta', hex: ['#107A3B', '#0e1311', '#F4F0E6', '#1d2a23'], signature: 'BV green, woven intrecciato, craft-forward', toneOnTone: true },
- { brand: 'Prada', hex: ['#000000', '#FF0000', '#F5F2EB'], signature: 'cream ground, scarlet accent, scientific austerity', toneOnTone: false },
- { brand: 'Saint Laurent', hex: ['#000000', '#FFFFFF'], signature: 'pure monochrome, sharp ultra-minimal', toneOnTone: false },
- { brand: 'Dior', hex: ['#1A1A1A', '#FAF9F6', '#A98B5B', '#D8D2C5'], signature: 'refined gray, soft gold accent, couture restraint', toneOnTone: true },
- { brand: 'Chanel', hex: ['#000000', '#FFFFFF'], signature: 'timeless black & white, high-contrast hierarchy', toneOnTone: false },
- { brand: 'Gucci', hex: ['#006837', '#A41E1A', '#FFFEF7', '#1a1a1a'], signature: 'green & red maximalism, ornate, warm ivory ground', toneOnTone: false },
- { brand: 'Celine', hex: ['#000000', '#FFFFFF', '#E0E0E0'], signature: 'minimalist gallery monochrome, sharp', toneOnTone: false },
- { brand: 'Loewe', hex: ['#9B7B4F', '#F2EDE2', '#2C1F0F', '#C9BFA7'], signature: 'soft cream, hand-craft warmth, airy', toneOnTone: true },
- { brand: 'Valentino', hex: ['#D70000', '#FFFFFF', '#000000'], signature: 'saturated romantic red, classical drama', toneOnTone: false },
- { brand: 'Toteme', hex: ['#1A1A1A', '#F5F2ED', '#D5CFC4'], signature: 'quiet restrained neutral, understated', toneOnTone: true },
- { brand: 'Khaite', hex: ['#3A3530', '#F2EFE9', '#C9BFB4'], signature: 'refined warm neutral, soft luxury', toneOnTone: true },
- { brand: 'Balenciaga', hex: ['#000000', '#F5F5F5', '#FF4F00'], signature: 'brutalist, anti-luxury, oversized orange jolt', toneOnTone: false },
- { brand: 'Burberry', hex: ['#A47C5A', '#FFFFFF', '#000000'], signature: 'heritage tan, check-pattern motif, clean', toneOnTone: true },
- { brand: 'Miu Miu', hex: ['#FFB6C1', '#FFF8F2', '#000000', '#FFCFD8'], signature: 'playful editorial pink, magazine warmth', toneOnTone: false },
- { brand: 'Jacquemus', hex: ['#F4D24F', '#FFFCF5', '#1a1a1a', '#F2E5A8'], signature: 'sun-yellow accent, French Riviera, cinematic', toneOnTone: false },
- { brand: 'Skims', hex: ['#C9B8A4', '#F5EFE6', '#3D352B', '#E8DDC9'], signature: 'nude flesh tones, soft conversion-led calm', toneOnTone: true },
- { brand: 'Gymshark', hex: ['#000000', '#1B5E20', '#FFFFFF'], signature: 'bold black, deep-green energy, high contrast', toneOnTone: false },
- { brand: 'Phoebe Philo', hex: ['#000000', '#F5F5F0', '#D8D2C5'], signature: 'typography-led minimal, refined off-white', toneOnTone: true },
+ // ── Fashion houses ───────────────────────────────────────────────────────
+ { brand: 'Louis Vuitton', country:'FR', est:1854, cat:'fashion', hex: ['#5A4632', '#C8A96A', '#F3ECDD', '#1a1a1a'], signature: 'monogram brown & tan, trunk-maker heritage, gilded restraint', toneOnTone: true },
+ { brand: 'Chanel', country:'FR', est:1910, cat:'fashion', hex: ['#000000', '#FFFFFF'], signature: 'timeless black & white, high-contrast hierarchy', toneOnTone: false },
+ { brand: 'Hermès', country:'FR', est:1837, cat:'fashion', hex: ['#FF4F00', '#1a1410', '#FFFEFA', '#E5DDD0'], signature: 'burnt-orange accent, saddle-stitch warmth, generous ground', toneOnTone: false },
+ { brand: 'Gucci', country:'IT', est:1921, cat:'fashion', hex: ['#006837', '#A41E1A', '#FFFEF7', '#1a1a1a'], signature: 'green & red maximalism, ornate, warm ivory ground', toneOnTone: false },
+ { brand: 'Prada', country:'IT', est:1913, cat:'fashion', hex: ['#000000', '#FF0000', '#F5F2EB'], signature: 'cream ground, scarlet accent, scientific austerity', toneOnTone: false },
+ { brand: 'Dior', country:'FR', est:1946, cat:'fashion', hex: ['#1A1A1A', '#FAF9F6', '#A98B5B', '#D8D2C5'], signature: 'refined gray, soft gold accent, couture restraint', toneOnTone: true },
+ { brand: 'Versace', country:'IT', est:1978, cat:'fashion', hex: ['#D4AF37', '#0a0a0a', '#A41E1A', '#F5EFE0'], signature: 'baroque gold on black, Medusa opulence, Greek-key maximalism', toneOnTone: false },
+ { brand: 'Burberry', country:'UK', est:1856, cat:'fashion', hex: ['#A47C5A', '#FFFFFF', '#000000'], signature: 'heritage tan, check-pattern motif, clean', toneOnTone: true },
+ { brand: 'Saint Laurent', country:'FR', est:1961, cat:'fashion', hex: ['#000000', '#FFFFFF'], signature: 'pure monochrome, sharp ultra-minimal', toneOnTone: false },
+ { brand: 'Balenciaga', country:'ES', est:1919, cat:'fashion', hex: ['#000000', '#F5F5F5', '#FF4F00'], signature: 'brutalist, anti-luxury, oversized orange jolt', toneOnTone: false },
+ { brand: 'Bottega Veneta', country:'IT', est:1966, cat:'fashion', hex: ['#107A3B', '#0e1311', '#F4F0E6', '#1d2a23'], signature: 'BV green, woven intrecciato, craft-forward', toneOnTone: true },
+ { brand: 'Fendi', country:'IT', est:1925, cat:'fashion', hex: ['#6B4F3A', '#2A211A', '#C9B79C', '#F2EADB'], signature: 'tobacco brown & cream, Roman pellicceria heritage', toneOnTone: true },
+ { brand: 'Givenchy', country:'FR', est:1952, cat:'fashion', hex: ['#0a0a0a', '#C8A24A', '#A41E1A', '#F4F1EA'], signature: 'parisian black with gold & oxblood, couture geometry', toneOnTone: false },
+ { brand: 'Valentino', country:'IT', est:1960, cat:'fashion', hex: ['#D70000', '#FFFFFF', '#000000'], signature: 'saturated romantic red, classical drama', toneOnTone: false },
+ { brand: 'Armani', country:'IT', est:1975, cat:'fashion', hex: ['#1C2A3A', '#8A8A86', '#15151A', '#E6E2DA'], signature: 'Armani greige & midnight navy, fluid minimal tailoring', toneOnTone: true },
+ { brand: 'Ralph Lauren', country:'US', est:1967, cat:'fashion', hex: ['#1B2A4A', '#9E1B32', '#F3ECDD', '#1a1a1a'], signature: 'ivy-league navy & polo crimson, equestrian heritage cream', toneOnTone: false },
+ { brand: 'Tom Ford', country:'US', est:2005, cat:'fashion', hex: ['#0a0a0a', '#6B4A2E', '#B08D4F', '#EDE6D8'], signature: 'glossy black, bronze & cognac, sensual modern glamour', toneOnTone: true },
+ { brand: 'Celine', country:'FR', est:1945, cat:'fashion', hex: ['#000000', '#FFFFFF', '#E0E0E0'], signature: 'minimalist gallery monochrome, sharp', toneOnTone: false },
+ { brand: 'Loewe', country:'ES', est:1846, cat:'fashion', hex: ['#9B7B4F', '#F2EDE2', '#2C1F0F', '#C9BFA7'], signature: 'soft cream, hand-craft warmth, airy', toneOnTone: true },
+ { brand: 'Alexander McQueen', country:'UK', est:1992, cat:'fashion', hex: ['#0a0a0a', '#7E1416', '#8C8C90', '#EDEAE3'], signature: 'gothic black & blood-red, romantic darkness, bone-grey', toneOnTone: false },
+
+ // ── Extra fashion houses (palette pool only — not on the top-20 selector) ─
+ { brand: 'Toteme', country:'SE', est:2014, cat:'fashion', hex: ['#1A1A1A', '#F5F2ED', '#D5CFC4'], signature: 'quiet restrained neutral, understated', toneOnTone: true },
+ { brand: 'Khaite', country:'US', est:2016, cat:'fashion', hex: ['#3A3530', '#F2EFE9', '#C9BFB4'], signature: 'refined warm neutral, soft luxury', toneOnTone: true },
+ { brand: 'Miu Miu', country:'IT', est:1993, cat:'fashion', hex: ['#FFB6C1', '#FFF8F2', '#000000', '#FFCFD8'], signature: 'playful editorial pink, magazine warmth', toneOnTone: false },
+ { brand: 'Jacquemus', country:'FR', est:2009, cat:'fashion', hex: ['#F4D24F', '#FFFCF5', '#1a1a1a', '#F2E5A8'], signature: 'sun-yellow accent, French Riviera, cinematic', toneOnTone: false },
+ { brand: 'Skims', country:'US', est:2019, cat:'fashion', hex: ['#C9B8A4', '#F5EFE6', '#3D352B', '#E8DDC9'], signature: 'nude flesh tones, soft conversion-led calm', toneOnTone: true },
+ { brand: 'Gymshark', country:'UK', est:2012, cat:'fashion', hex: ['#000000', '#1B5E20', '#FFFFFF'], signature: 'bold black, deep-green energy, high contrast', toneOnTone: false },
+ { brand: 'Phoebe Philo', country:'UK', est:2023, cat:'fashion', hex: ['#000000', '#F5F5F0', '#D8D2C5'], signature: 'typography-led minimal, refined off-white', toneOnTone: true },
+
+ // ── Wallcovering / heritage-textile houses ────────────────────────────────
+ { brand: 'Phillip Jeffries', country:'US', est:1976, cat:'wallcovering', hex: ['#C9B08A', '#6E563B', '#F1E9D8', '#A8916E'], signature: 'natural grasscloth — flax, sand & raffia neutrals, textural luxury', toneOnTone: true },
+ { brand: 'Schumacher', country:'US', est:1889, cat:'wallcovering', hex: ['#A8322E', '#2E6B4F', '#C8A24A', '#F4ECDB'], signature: 'archival americana — coral red, jade & antique-gold chintz', toneOnTone: false },
+ { brand: 'Cole & Son', country:'UK', est:1875, cat:'wallcovering', hex: ['#1F3A52', '#C2A878', '#F2EBDA', '#2C3B30'], signature: 'english heritage — ink-navy & parchment, woods & hummingbird motifs', toneOnTone: true },
+ { brand: 'Thibaut', country:'US', est:1886, cat:'wallcovering', hex: ['#3E6B4A', '#CDBE9A', '#F2ECDC', '#2A3A2C'], signature: 'classic botanical — leaf-green & wheat, american traditional', toneOnTone: true },
+ { brand: 'Zoffany', country:'UK', est:1895, cat:'wallcovering', hex: ['#6B4A33', '#B79A72', '#5E6E4E', '#EFE7D6'], signature: 'edwardian opulence — chestnut, gilt-sage & antique cream', toneOnTone: true },
+ { brand: 'Farrow & Ball', country:'UK', est:1946, cat:'wallcovering', hex: ['#2E5A55', '#EDE7D8', '#7A6A56', '#9FB0A6'], signature: 'chalky archival paint tones — inchyra teal, off-white & clay', toneOnTone: true },
+ { brand: 'Brunschwig & Fils', country:'US', est:1909, cat:'wallcovering', hex: ['#A8322E', '#2E4A6B', '#C9B189', '#F2EBDA'], signature: 'document-print classicism — lacquer red, delft blue & linen', toneOnTone: false },
+ { brand: 'de Gournay', country:'UK', est:1986, cat:'wallcovering', hex: ['#C2A878', '#6E563B', '#5E7E5E', '#F1E9D8'], signature: 'hand-painted chinoiserie — celadon, tea-stain & gilt, scenic luxe', toneOnTone: true },
+ { brand: 'Fromental', country:'UK', est:2005, cat:'wallcovering', hex: ['#274A6B', '#C8A24A', '#6B4A33', '#EEE6D4'], signature: 'embroidered silk panels — sapphire, antique gold & espresso', toneOnTone: false },
+ { brand: 'Pierre Frey', country:'FR', est:1935, cat:'wallcovering', hex: ['#B5322E', '#C8A24A', '#2E4A6B', '#F4ECDB'], signature: 'french painterly maximalism — vermilion, ochre & cobalt', toneOnTone: false },
];
// pickFashionPalette({ toneOnToneOnly: true }) restricts to the soft/archival
diff --git a/server.js b/server.js
index 4287b22..d106449 100644
--- a/server.js
+++ b/server.js
@@ -1128,6 +1128,44 @@ setInterval(() => load(true), 30000);
// check as a CRITICAL bypass (anyone could mint a token with {role:"admin"}).
const { isAdmin, requireAdmin } = require('./src/admin-gate');
+// ── Style Guides — Brand Selector (admin) ──────────────────────────────────
+// A curated picker of fashion + wallcovering houses whose palettes/signatures
+// seed the generative pipeline. Three live actions on the page:
+// • Generate → POST /api/generator/batch (new SDXL designs in the houses' look)
+// • Apply → POST /api/design/:id/recolor (restyle an existing studio design)
+// • Export → portable style-guide JSON / Figma block (client-side)
+// Lives behind the same admin gate as the curators. Reachable two ways:
+// /admin/styleguides AND the bare root of the styleguides.wallco.ai vhost.
+app.get('/', (req, res, next) => {
+ if (/^styleguides\./i.test(req.hostname || '')) {
+ if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+ return res.sendFile(path.join(__dirname, 'public', 'admin', 'styleguides.html'));
+ }
+ next();
+});
+app.get('/admin/styleguides', (req, res) => {
+ if (!isAdmin(req)) return res.status(404).type('html').send('<h1>404</h1>');
+ res.sendFile(path.join(__dirname, 'public', 'admin', 'styleguides.html'));
+});
+app.get('/api/styleguides/brands', (req, res) => {
+ try {
+ const { FASHION_PALETTES } = require('./scripts/fashion_palettes.js');
+ // Same kebab transform RECOLOR_BRANDS uses, so the page's data-brand keys
+ // match the recolor endpoint's server-side keys exactly.
+ const kebab = s => s.normalize('NFD').replace(/[̀-ͯ]/g, '')
+ .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
+ // Curated display order — mirrors the Brand Selector design (Paper file).
+ const FASHION_TOP = ['Louis Vuitton','Chanel','Hermès','Gucci','Prada','Dior','Versace','Burberry','Saint Laurent','Balenciaga','Bottega Veneta','Fendi','Givenchy','Valentino','Armani','Ralph Lauren','Tom Ford','Celine','Loewe','Alexander McQueen'];
+ const WALL_TOP = ['Phillip Jeffries','Schumacher','Cole & Son','Thibaut','Zoffany','Farrow & Ball','Brunschwig & Fils','de Gournay','Fromental','Pierre Frey'];
+ const byBrand = {}; FASHION_PALETTES.forEach(p => { byBrand[p.brand] = p; });
+ const pick = names => names.map(n => byBrand[n]).filter(Boolean).map(p => ({
+ key: kebab(p.brand), label: p.brand, cat: p.cat || 'fashion',
+ country: p.country || null, est: p.est || null, hex: p.hex, signature: p.signature,
+ }));
+ res.json({ ok: true, fashion: pick(FASHION_TOP), wallcovering: pick(WALL_TOP) });
+ } catch (e) { res.status(500).json({ ok: false, error: e.message }); }
+});
+
// ── Global PDP theme takeover (Steve 2026-05-31, /admin/theme-gallery "Set Live").
// The admin can promote one of the 17 theme-variant templates to BE the live
// customer PDP. When set, the bare /design/:id route serves that static template
← 6250128 generator_tick: retry psql on boot-race PG-socket-not-ready
·
back to Wallco Ai
·
feat(styleguides): open to public with per-IP render cap (5 5a812ec →