← back to Wallco Ai
public/luxe-c-gallery.html
167 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Luxe-C Gallery — Root vs 1838 Flocked Velvet</title>
<style>
:root { --bg:#fafaf8; --fg:#1a1a1a; --muted:#6a6a6a; --card:#fff; --border:#e8e6e1; --accent:#8b1f1f; --gold:#c9a14b; --card-min:420px; }
* { box-sizing: border-box; }
body { margin:0; background:var(--bg); color:var(--fg); font:14px/1.5 -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif; }
header { padding:24px 32px 16px; border-bottom:1px solid var(--border); background:var(--card); position:sticky; top:0; z-index:10; }
header h1 { margin:0 0 4px; font-size:20px; font-weight:600; letter-spacing:-0.01em; }
header h1 .badge { display:inline-block; padding:2px 8px; font-size:11px; font-weight:600; border-radius:10px; background:#1a1a1a; color:#fff; margin-left:8px; vertical-align:middle; }
header .stats { color:var(--muted); font-size:13px; }
header .stats b { color:var(--fg); font-weight:600; }
.filters { margin-top:12px; display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
.filters select, .filters input { padding:6px 10px; border:1px solid var(--border); border-radius:4px; background:var(--card); font:inherit; }
.filters label { font-size:12px; color:var(--muted); margin-right:4px; }
main { padding:24px 32px 64px; }
.grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(var(--card-min), 1fr)); gap:24px; }
.filters input[type=range] { accent-color:var(--gold); vertical-align:middle; padding:0; }
.pair { background:var(--card); border:1px solid var(--border); border-radius:8px; overflow:hidden; }
.pair-imgs { display:grid; grid-template-columns:1fr 1fr; gap:0; }
.pair-imgs > div { position:relative; aspect-ratio:1; background:#f0eee8; overflow:hidden; }
.pair-imgs img { width:100%; height:100%; object-fit:cover; display:block; }
.pair-imgs .label {
position:absolute; top:6px; left:6px; padding:2px 8px; font-size:10px;
letter-spacing:0.08em; text-transform:uppercase; border-radius:2px;
background:rgba(0,0,0,0.7); color:#fff; font-weight:600;
}
.pair-imgs .label.luxe { background:rgba(140,40,60,0.92); }
.pair-imgs .ground {
position:absolute; bottom:6px; right:6px; padding:2px 7px; font-size:10px;
letter-spacing:0.06em; background:rgba(255,255,255,0.92); color:#1a1a1a;
border-radius:2px; font-weight:600;
}
.pair-meta { padding:10px 12px; }
.pair-meta .ids { display:flex; justify-content:space-between; align-items:center; font-size:12px; margin-bottom:6px; }
.pair-meta .ids a { color:var(--accent); text-decoration:none; font-weight:600; }
.pair-meta .ids a:hover { text-decoration:underline; }
.pair-meta .motif { color:var(--muted); font-size:11.5px; line-height:1.4; }
.arrow { color:var(--muted); margin:0 4px; }
.empty { padding:40px; text-align:center; color:var(--muted); }
.live-tag { display:inline-block; width:8px; height:8px; border-radius:50%; background:#1d6b3a; margin-right:6px; animation:pulse 1.6s infinite; vertical-align:middle; }
@keyframes pulse { 0%,100% { opacity:1 } 50% { opacity:0.3 } }
</style>
</head>
<body>
<header>
<h1>Luxe-C Gallery <span class="badge" id="count">…</span></h1>
<div class="stats"><span class="live-tag" id="live"></span>
<b>Root</b> (left) → <b>1838 Flocked Velvet luxe</b> (right). 5 heritage-house variants in <code>scripts/gen-luxe.js</code>; variant C locks to 2-color flocked silhouettes on a real DW silk/grasscloth/linen/raffia/cork ground. Source unpublished automatically after luxe lands.
<br>Avg <b id="avg">…</b>s per regen · <b id="totalCost">…</b> spent on Gemini Flash Image.
</div>
<div class="filters">
<label>Ground</label>
<select id="groundFilter"><option value="">All grounds</option></select>
<label>Sort</label>
<select id="sortBy">
<option value="newest">Newest first</option>
<option value="root">Root ID ↑</option>
<option value="ground">Ground category</option>
</select>
<label>Show</label>
<select id="limit">
<option value="60">60</option>
<option value="120">120</option>
<option value="240">240</option>
<option value="0">All</option>
</select>
<label>Density</label>
<input type="range" id="density" min="320" max="680" step="20" title="pair card size">
<button id="refresh" style="margin-left:auto; padding:6px 12px; border:1px solid var(--border); background:var(--card); border-radius:4px; cursor:pointer; font:inherit;">Refresh</button>
</div>
</header>
<main>
<div class="grid" id="grid"><div class="empty">Loading…</div></div>
</main>
<script>
async function loadData() {
const r = await fetch('/api/luxe-c-gallery');
if (!r.ok) throw new Error('fetch failed: ' + r.status);
return r.json();
}
function render(items) {
const grid = document.getElementById('grid');
document.getElementById('count').textContent = items.length;
if (!items.length) {
grid.innerHTML = '<div class="empty">No luxe pairs yet — kick off <code>node scripts/roll-luxe-c.js</code> first.</div>';
return;
}
grid.innerHTML = items.map(p => `
<div class="pair">
<div class="pair-imgs">
<div>
<span class="label">Root #${p.root_id}</span>
<img loading="lazy" src="/designs/img/by-id/${p.root_id}?asis=1" alt="root ${p.root_id}">
</div>
<div>
<span class="label luxe">Luxe #${p.new_id}</span>
<span class="ground">${p.ground || '—'}</span>
<img loading="lazy" src="/designs/img/by-id/${p.new_id}" alt="luxe ${p.new_id}">
</div>
</div>
<div class="pair-meta">
<div class="ids">
<a href="/design/${p.root_id}?asis=1">root ${p.root_id}</a>
<span class="arrow">→ flocked velvet →</span>
<a href="/design/${p.new_id}">luxe ${p.new_id}</a>
</div>
<div class="motif">${p.motif || ''}</div>
</div>
</div>
`).join('');
}
let DATA = [];
function apply() {
const ground = document.getElementById('groundFilter').value;
const sort = document.getElementById('sortBy').value;
const limit = parseInt(document.getElementById('limit').value, 10);
let items = DATA.slice();
if (ground) items = items.filter(p => p.ground === ground);
if (sort === 'newest') items.sort((a,b) => b.new_id - a.new_id);
else if (sort === 'root') items.sort((a,b) => a.root_id - b.root_id);
else if (sort === 'ground') items.sort((a,b) => (a.ground||'').localeCompare(b.ground||''));
if (limit > 0) items = items.slice(0, limit);
render(items);
}
async function refresh() {
try {
const data = await loadData();
DATA = data.items || [];
const totalMs = DATA.reduce((s,p) => s + (p.duration_ms||0), 0);
document.getElementById('avg').textContent = DATA.length ? (totalMs / DATA.length / 1000).toFixed(1) : '—';
// Cost: Gemini 2.5 Flash Image ≈ $0.04 / call
document.getElementById('totalCost').textContent = '$' + (DATA.length * 0.04).toFixed(2);
// Repopulate ground filter (keep current selection)
const sel = document.getElementById('groundFilter');
const current = sel.value;
const grounds = [...new Set(DATA.map(p => p.ground).filter(Boolean))].sort();
sel.innerHTML = '<option value="">All grounds (' + DATA.length + ')</option>' +
grounds.map(g => '<option value="' + g + '">' + g + ' (' + DATA.filter(p => p.ground === g).length + ')</option>').join('');
sel.value = current;
apply();
} catch (e) {
document.getElementById('grid').innerHTML = '<div class="empty">Failed to load: ' + e.message + '</div>';
}
}
// sort + density persist (Steve standing rule: every product grid; both persist)
document.getElementById('sortBy').value = localStorage.getItem('luxeC_sort') || 'newest';
document.getElementById('density').value = localStorage.getItem('luxeC_density') || '420';
document.documentElement.style.setProperty('--card-min', document.getElementById('density').value + 'px');
document.getElementById('groundFilter').addEventListener('change', apply);
document.getElementById('sortBy').addEventListener('change', () => { localStorage.setItem('luxeC_sort', document.getElementById('sortBy').value); apply(); });
document.getElementById('limit').addEventListener('change', apply);
document.getElementById('density').addEventListener('input', () => {
localStorage.setItem('luxeC_density', document.getElementById('density').value);
document.documentElement.style.setProperty('--card-min', document.getElementById('density').value + 'px');
});
document.getElementById('refresh').addEventListener('click', refresh);
// Auto-refresh every 8s so rollout progress shows up live
setInterval(refresh, 8000);
refresh();
</script>
</body>
</html>