← back to Wallco Ai
public/admin/workspace.html
203 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Admin Workspace — wallco.ai</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root{--bg:#0f1117;--ink:#e8eaf2;--mut:#8b93a7;--line:#262b3a;--card:#161a26;--accent:#34cbf4;}
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:
radial-gradient(1200px 600px at 20% -10%,#1a2030,transparent),var(--bg);
color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;overflow:hidden}
header.ws{position:fixed;top:0;left:0;right:0;height:52px;z-index:50;display:flex;align-items:center;
gap:12px;padding:0 16px;background:rgba(15,17,23,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--line)}
h1{font-size:15px;margin:0;font-weight:650;letter-spacing:.2px}
.sub{color:var(--mut);font-size:12px}
button,select{font:inherit;color:var(--ink);background:#1c2030;border:1px solid var(--line);
border-radius:8px;padding:7px 12px;cursor:pointer}
button.primary{background:var(--accent);color:#06202a;border-color:var(--accent);font-weight:650}
button:hover{border-color:var(--accent)}
.spacer{flex:1}
main{position:fixed;inset:52px 0 0 0;overflow:hidden}
.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;
color:var(--mut);gap:14px;text-align:center;padding:24px}
.empty h2{color:var(--ink);font-weight:600;margin:0}
/* Windows */
.win{position:absolute;background:var(--card);border:1px solid var(--line);border-radius:12px;
box-shadow:0 22px 70px rgba(0,0,0,.5);display:flex;flex-direction:column;overflow:hidden;min-width:280px;min-height:180px}
.win.focus{border-color:var(--accent);box-shadow:0 22px 80px rgba(52,203,244,.18),0 22px 70px rgba(0,0,0,.5)}
.win-bar{display:flex;align-items:center;gap:8px;padding:7px 9px;cursor:grab;user-select:none;
background:linear-gradient(#1c2030,#141826);border-bottom:1px solid var(--line)}
.win-bar:active{cursor:grabbing}
.win-grip{opacity:.6;letter-spacing:-2px}
.win-title{font-weight:600;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.win-btn{border:0;background:rgba(255,255,255,.10);color:var(--ink);width:24px;height:22px;border-radius:6px;
cursor:pointer;font-size:12px;line-height:1;padding:0}
.win-btn:hover{background:rgba(255,255,255,.22)}
.win-body{flex:1;position:relative;background:#fff}
.win-body iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#fff}
.win-resize{position:absolute;right:2px;bottom:2px;width:16px;height:16px;cursor:nwse-resize;
background:linear-gradient(135deg,transparent 50%,var(--mut) 50%);border-radius:0 0 8px 0;opacity:.7}
.win.min .win-body,.win.min .win-resize{display:none}
.win.min{height:auto!important;min-height:0}
.chip{font-size:11px;color:var(--mut);border:1px solid var(--line);border-radius:999px;padding:3px 9px}
</style>
</head>
<body>
<header class="ws">
<h1>🪟 Admin Workspace</h1>
<span class="sub" id="count">0 panels</span>
<span class="spacer"></span>
<select id="addSel" title="Pick a grid/page to open as a panel">
<option value="">+ Add panel…</option>
</select>
<button id="tileBtn" title="Auto-arrange all panels in a tidy grid">▦ Tile</button>
<button id="cascadeBtn" title="Cascade panels diagonally">▧ Cascade</button>
<button id="clearBtn" title="Close all panels">Clear</button>
</header>
<main id="stage">
<div class="empty" id="empty">
<h2>Empty workspace</h2>
<div class="sub">Use <strong>+ Add panel</strong> to open any admin grid as a movable, resizable window.<br>
Drag the title bar to move · drag the corner to resize · layout auto-saves.</div>
</div>
</main>
<script>
// ---- Catalog of openable admin grids/pages ----
const PAGES = [
['Design Curator', '/admin/design-curator'],
['Drunk Curator', '/admin/drunk-curator'],
['Dogs Curator', '/admin/dogs-curator'],
['Botanical Curator', '/admin/botanical-wonders-curator'],
['Ghost Review', '/admin/ghost-review'],
['Edges Review', '/admin/edges-review'],
['Bleed Review', '/admin/bleed-review'],
['Defect Registry', '/admin/defect-registry'],
['Rooms Gallery', '/admin/rooms'],
['Theme Gallery', '/admin/theme-gallery'],
['Elements', '/admin/elements'],
['Inspirations', '/admin/inspirations'],
['Etsy Bucket', '/admin/etsy-bucket'],
['Status Browser', '/admin/status-browser'],
['Designs grid (public)', '/designs'],
];
const STORE = 'ws-admin-layout-v1';
const $ = s => document.querySelector(s);
const stage = $('#stage');
let z = 10, wins = [];
PAGES.forEach(([label,url]) => {
const o = document.createElement('option'); o.value = url; o.textContent = label; $('#addSel').appendChild(o);
});
function save(){
const data = wins.map(w => ({
url:w.dataset.url, title:w.dataset.title,
left:parseInt(w.style.left), top:parseInt(w.style.top),
w:parseInt(w.style.width), h:parseInt(w.style.height),
min:w.classList.contains('min'), z:+w.style.zIndex||10
}));
try{ localStorage.setItem(STORE, JSON.stringify(data)); }catch(e){}
$('#count').textContent = wins.length + (wins.length===1?' panel':' panels');
$('#empty').style.display = wins.length ? 'none' : '';
}
function focusWin(w){ wins.forEach(x=>x.classList.remove('focus')); w.classList.add('focus'); w.style.zIndex = ++z; }
function makeWin(opt){
const w = document.createElement('div'); w.className='win';
w.dataset.url = opt.url; w.dataset.title = opt.title || labelFor(opt.url);
w.style.left=(opt.left??(40+wins.length*28))+'px';
w.style.top =(opt.top ??(40+wins.length*28))+'px';
w.style.width =(opt.w??560)+'px';
w.style.height=(opt.h??460)+'px';
w.style.zIndex = opt.z || (++z);
w.innerHTML =
'<div class="win-bar">'+
'<span class="win-grip">⠿</span>'+
'<span class="win-title"></span>'+
'<a class="win-btn" title="Open full page" target="_blank" rel="noopener" href="'+esc(opt.url)+'">↗</a>'+
'<button class="win-btn b-reload" title="Reload">⟳</button>'+
'<button class="win-btn b-min" title="Collapse">—</button>'+
'<button class="win-btn b-close" title="Close">✕</button>'+
'</div>'+
'<div class="win-body"><iframe src="'+esc(opt.url)+'" loading="lazy" referrerpolicy="same-origin"></iframe></div>'+
'<div class="win-resize" title="Drag to resize"></div>';
w.querySelector('.win-title').textContent = w.dataset.title;
if (opt.min) w.classList.add('min');
stage.appendChild(w); wins.push(w);
const bar = w.querySelector('.win-bar');
// drag
let dx,dy,ox,oy,drag=false;
bar.addEventListener('pointerdown', e=>{
if(e.target.closest('.win-btn')) return;
drag=true; focusWin(w); dx=e.clientX; dy=e.clientY; ox=parseInt(w.style.left); oy=parseInt(w.style.top);
bar.setPointerCapture(e.pointerId); e.preventDefault();
});
bar.addEventListener('pointermove', e=>{
if(!drag) return;
let nl=ox+(e.clientX-dx), nt=oy+(e.clientY-dy);
nl=Math.max(2-w.offsetWidth+80, Math.min(nl, innerWidth-60));
nt=Math.max(0, Math.min(nt, innerHeight-90));
w.style.left=nl+'px'; w.style.top=nt+'px';
});
bar.addEventListener('pointerup', ()=>{ if(drag){drag=false; save();} });
// resize
const rz=w.querySelector('.win-resize'); let rw,rh,rx,ry,res=false;
rz.addEventListener('pointerdown', e=>{
res=true; focusWin(w); rx=e.clientX; ry=e.clientY; rw=w.offsetWidth; rh=w.offsetHeight;
rz.setPointerCapture(e.pointerId); e.preventDefault();
});
rz.addEventListener('pointermove', e=>{
if(!res) return;
w.style.width =Math.max(280, rw+(e.clientX-rx))+'px';
w.style.height=Math.max(180, rh+(e.clientY-ry))+'px';
});
rz.addEventListener('pointerup', ()=>{ if(res){res=false; save();} });
// focus on any click
w.addEventListener('pointerdown', ()=>focusWin(w), true);
// buttons
w.querySelector('.b-reload').addEventListener('click', ()=>{ const f=w.querySelector('iframe'); f.src=f.src; });
w.querySelector('.b-min').addEventListener('click', ()=>{ w.classList.toggle('min'); save(); });
w.querySelector('.b-close').addEventListener('click', ()=>{ w.remove(); wins=wins.filter(x=>x!==w); save(); });
focusWin(w); save();
return w;
}
function labelFor(url){ const p = PAGES.find(x=>x[1]===url); return p?p[0]:url; }
function esc(s){ return String(s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c])); }
// tile / cascade
function tile(){
const n=wins.length; if(!n) return;
const cols=Math.ceil(Math.sqrt(n)), rows=Math.ceil(n/cols);
const W=innerWidth, H=innerHeight-52, gw=W/cols, gh=H/rows;
wins.forEach((w,i)=>{ const c=i%cols, r=Math.floor(i/cols);
w.classList.remove('min');
w.style.left=Math.round(c*gw+4)+'px'; w.style.top=Math.round(r*gh+4)+'px';
w.style.width=Math.round(gw-8)+'px'; w.style.height=Math.round(gh-8)+'px'; });
save();
}
function cascade(){ wins.forEach((w,i)=>{ w.classList.remove('min');
w.style.left=(40+i*30)+'px'; w.style.top=(40+i*30)+'px'; w.style.width='560px'; w.style.height='460px';
w.style.zIndex=++z; }); save(); }
$('#addSel').addEventListener('change', e=>{ const url=e.target.value; if(url){ makeWin({url}); e.target.value=''; } });
$('#tileBtn').addEventListener('click', tile);
$('#cascadeBtn').addEventListener('click', cascade);
$('#clearBtn').addEventListener('click', ()=>{ if(wins.length && confirm('Close all panels?')){ wins.forEach(w=>w.remove()); wins=[]; save(); } });
// restore
(function init(){
let data=[]; try{ data=JSON.parse(localStorage.getItem(STORE)||'[]'); }catch(e){}
data.sort((a,b)=>(a.z||0)-(b.z||0)).forEach(makeWin);
save();
})();
</script>
</body>
</html>