← back to Cc Command Center
public/index.html
206 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Claude Code — Command Center</title>
<style>
:root{
--bg:#1c1c1f; --panel:#212125; --panel2:#26262b; --line:#33333a;
--ink:#e9e9ec; --mut:#9a9aa3; --mut2:#6f6f78; --accent:#d8674a;
--blue:#5b8def; --cell:#2a2a30; --c1:#1d3a63; --c2:#2f5ea8; --c3:#4f86e0; --c4:#7fb0ff;
--shadow:0 18px 50px rgba(0,0,0,.55);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{background:var(--bg);color:var(--ink);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif;overflow:hidden}
.app{display:grid;grid-template-columns:236px 1fr;height:100vh}
/* sidebar */
.side{background:#19191c;border-right:1px solid var(--line);padding:10px;display:flex;flex-direction:column;gap:4px;overflow:auto}
.winbtns{display:flex;gap:7px;padding:6px 6px 12px}
.winbtns i{width:12px;height:12px;border-radius:50%;display:inline-block}
.winbtns i:nth-child(1){background:#ff5f57}.winbtns i:nth-child(2){background:#febc2e}.winbtns i:nth-child(3){background:#28c840}
.tabs{display:flex;gap:4px;background:#212125;border:1px solid var(--line);border-radius:9px;padding:3px;margin-bottom:8px}
.tabs button{flex:1;background:none;border:0;color:var(--mut);padding:5px;border-radius:6px;font-size:12.5px;cursor:pointer}
.tabs button.on{background:#34343b;color:var(--ink)}
.navi{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:8px;color:var(--ink);cursor:pointer;font-size:13.5px}
.navi:hover{background:#26262b}
.navi .ic{width:15px;text-align:center;color:var(--mut)}
.seclabel{color:var(--mut2);font-size:11px;letter-spacing:.08em;text-transform:uppercase;margin:14px 9px 4px;font-weight:600}
.rec{display:flex;align-items:center;gap:8px;padding:6px 9px;border-radius:7px;color:var(--mut);cursor:pointer;font-size:12.7px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.rec:hover{background:#26262b;color:var(--ink)}
.rec .dot{width:6px;height:6px;border-radius:50%;background:#4a4a52;flex:none}
.who{margin-top:auto;display:flex;align-items:center;gap:9px;padding:9px;border-top:1px solid var(--line);color:var(--mut);font-size:12.5px}
.who .av{width:20px;height:20px;border-radius:6px;background:linear-gradient(135deg,#d8674a,#b23e6e);color:#fff;display:grid;place-items:center;font-size:11px;font-weight:700}
/* main */
.main{position:relative;overflow:hidden}
.canvas{position:absolute;inset:0;overflow:auto;padding:34px 40px}
h1{font-size:21px;font-weight:600;margin:0 0 22px;display:flex;align-items:center;gap:10px}
.star{color:var(--accent)}
.dash{max-width:760px}
.dashhead{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}
.pillrow{display:flex;gap:6px}
.pill{background:#2a2a30;border:1px solid var(--line);color:var(--mut);font-size:12px;padding:3px 10px;border-radius:7px;cursor:pointer}
.pill.on{color:var(--ink);background:#34343b}
.card{background:var(--panel);border:1px solid var(--line);border-radius:12px;overflow:hidden}
.grid{display:grid;grid-template-columns:repeat(4,1fr)}
.cell{padding:13px 15px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}
.cell:nth-child(4n){border-right:0}
.cell .k{color:var(--mut);font-size:12px;margin-bottom:5px}
.cell .v{font-size:19px;font-weight:600;letter-spacing:-.01em}
.heatwrap{padding:15px}
.heat{display:grid;grid-auto-flow:column;grid-template-rows:repeat(7,11px);gap:3px;overflow-x:auto}
.h{width:11px;height:11px;border-radius:2.5px;background:var(--cell)}
.h.l1{background:var(--c1)}.h.l2{background:var(--c2)}.h.l3{background:var(--c3)}.h.l4{background:var(--c4)}
.flavor{color:var(--mut);font-size:12.5px;padding:0 15px 15px}
.models{display:flex;flex-wrap:wrap;gap:7px;margin-top:16px}
.mchip{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:6px 11px;font-size:12.5px;color:var(--mut)}
.mchip b{color:var(--ink);font-weight:600}
.hint{margin-top:26px;color:var(--mut2);font-size:12.5px}
.toolbar{margin-top:14px;display:flex;gap:8px;flex-wrap:wrap}
.tbtn{background:#2a2a30;border:1px solid var(--line);color:var(--ink);padding:7px 13px;border-radius:8px;cursor:pointer;font-size:12.5px}
.tbtn:hover{background:#34343b}
/* floating session windows */
.swin{position:absolute;width:430px;height:300px;background:var(--panel);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow);display:flex;flex-direction:column;overflow:hidden}
.swin .bar{display:flex;align-items:center;gap:8px;padding:9px 11px;border-bottom:1px solid var(--line);cursor:move;background:var(--panel2)}
.swin .bar .ttl{font-size:12.7px;font-weight:600;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.swin .bar .proj{font-size:10.5px;color:var(--mut2);background:#2a2a30;padding:2px 7px;border-radius:5px;border:1px solid var(--line);max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.swin .bar .x{cursor:pointer;color:var(--mut);width:18px;text-align:center}
.swin .bar .x:hover{color:var(--ink)}
.swin .body{flex:1;overflow:auto;padding:12px 13px;font-size:12.7px;color:var(--mut)}
.msg{margin-bottom:10px}
.msg .role{font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut2);margin-bottom:3px}
.msg.u .bub{background:#2f5ea8;color:#eaf1ff;display:inline-block;padding:7px 11px;border-radius:10px}
.msg.a .bub{color:var(--ink)}
.swin .foot{border-top:1px solid var(--line);padding:8px 11px;display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--mut2)}
.swin .resz{position:absolute;right:2px;bottom:2px;width:15px;height:15px;cursor:nwse-resize;
background:linear-gradient(135deg,transparent 50%,var(--mut2) 50%,var(--mut2) 60%,transparent 60%,transparent 72%,var(--mut2) 72%,var(--mut2) 82%,transparent 82%)}
.gen{position:absolute;right:16px;bottom:12px;color:var(--mut2);font-size:11px}
</style>
</head>
<body>
<div class="app">
<aside class="side">
<div class="winbtns"><i></i><i></i><i></i></div>
<div class="tabs"><button>Chat</button><button>Cowork</button><button class="on">Code</button></div>
<div class="navi"><span class="ic">+</span> New session</div>
<div class="navi"><span class="ic">⟳</span> Routines</div>
<div class="navi"><span class="ic">▦</span> Artifacts</div>
<div class="navi"><span class="ic">⚙</span> Customize</div>
<div class="seclabel">Recents</div>
<div id="recents"></div>
<div class="who"><span class="av" id="avatar">S</span><span id="whoname">Steve · Designer Wallcoverings</span></div>
</aside>
<main class="main">
<div class="canvas" id="canvas">
<h1><span class="star">✳</span> What's up next, <span id="hname">Steve</span>?</h1>
<div class="dash">
<div class="card">
<div class="dashhead" style="padding:11px 15px 0">
<div class="pillrow"><span class="pill on">Overview</span><span class="pill">Models</span></div>
<div class="pillrow"><span class="pill on">All</span><span class="pill">30d</span><span class="pill">7d</span></div>
</div>
<div class="grid" id="cards" style="margin-top:11px"></div>
<div class="heatwrap"><div class="heat" id="heat"></div></div>
<div class="flavor" id="flavor"></div>
</div>
<div class="models" id="models"></div>
<div class="hint">Open a session from <b>Recents</b> to pop it out as a floating window — then drag the title bar, resize from the corner, or tile them all.</div>
<div class="toolbar">
<button class="tbtn" onclick="openTop(3)">Open recent 3</button>
<button class="tbtn" onclick="tileWins()">Tile</button>
<button class="tbtn" onclick="cascadeWins()">Cascade</button>
<button class="tbtn" onclick="closeAll()">Close all</button>
</div>
</div>
</div>
<div class="gen" id="gen"></div>
</main>
</div>
<script>
let DATA=null, wins=[], z=10, cascade=0;
const $=s=>document.querySelector(s);
const esc=s=>(s||"").replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
const nf=n=>n.toLocaleString();
function heatClass(c){ if(!c)return''; if(c<5)return'l1'; if(c<15)return'l2'; if(c<40)return'l3'; return'l4'; }
fetch('./data/stats.json').then(r=>r.json()).then(d=>{ DATA=d; render();
if(location.hash==='#demo'){ openTop(4); setTimeout(tileWins,50); } });
function render(){
const c=DATA.cards;
$('#hname').textContent=DATA.user;
$('#whoname').firstChild ? $('#whoname').textContent=DATA.user+' · Designer Wallcoverings':0;
$('#avatar').textContent=DATA.user[0];
const cards=[
['Sessions', nf(c.sessions)],['Messages', nf(c.messages)],
['Total tokens', tok(c.total_tokens)],['Active days', nf(c.active_days)],
['Current streak', c.current_streak+'d'],['Longest streak', c.longest_streak+'d'],
['Peak hour', c.peak_hour],['Favorite model', c.favorite_model],
];
$('#cards').innerHTML=cards.map(([k,v])=>`<div class="cell"><div class="k">${k}</div><div class="v">${v}</div></div>`).join('');
$('#heat').innerHTML=DATA.heatmap.map(h=>`<div class="h ${heatClass(h.count)}" title="${h.date}: ${h.count}"></div>`).join('');
$('#flavor').textContent=DATA.flavor;
$('#models').innerHTML=DATA.models.map(m=>`<span class="mchip"><b>${esc(m.model)}</b> · ${nf(m.count)} msgs</span>`).join('');
$('#recents').innerHTML=DATA.recents.map((s,i)=>
`<div class="rec" title="${esc(s.title)}" onclick="openWin(${i})"><span class="dot"></span>${esc(s.title)}</div>`).join('');
$('#gen').textContent='generated '+DATA.generated_at;
}
function tok(n){ if(n>=1e9)return (n/1e9).toFixed(1)+'B'; if(n>=1e6)return (n/1e6).toFixed(1)+'M'; if(n>=1e3)return (n/1e3).toFixed(1)+'K'; return n; }
function openWin(i){
const s=DATA.recents[i]; if(!s) return;
if(wins.find(w=>w.dataset.sid===s.id)) return; // no dupes
const el=document.createElement('div'); el.className='swin'; el.dataset.sid=s.id;
const x=70+(cascade*30)%360, y=60+(cascade*28)%240; cascade++;
el.style.left=x+'px'; el.style.top=y+'px'; el.style.zIndex=++z;
el.innerHTML=`
<div class="bar"><span class="proj">${esc(s.project)}</span>
<span class="ttl">${esc(s.title)}</span><span class="x">✕</span></div>
<div class="body">
${s.snippet?`<div class="msg u"><div class="role">You</div><div class="bub">${esc(s.snippet)}</div></div>`:''}
<div class="msg a"><div class="role"><span style="color:var(--accent)">✳</span> Claude</div>
<div class="bub">${esc(s.last)||'(session resumed)'}</div></div>
</div>
<div class="foot"><span>${nf(s.user_msgs)} prompts · ${tok(s.tokens)} tokens</span>
<span>${s.ts?new Date(s.ts).toLocaleString(undefined,{month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}):''}</span></div>
<div class="resz"></div>`;
$('.main').appendChild(el); wins.push(el);
el.addEventListener('mousedown',()=>el.style.zIndex=++z);
el.querySelector('.x').onclick=()=>{ el.remove(); wins=wins.filter(w=>w!==el); };
drag(el, el.querySelector('.bar'));
resize(el, el.querySelector('.resz'));
}
function openTop(n){ for(let i=0;i<n;i++) openWin(i); }
function closeAll(){ wins.forEach(w=>w.remove()); wins=[]; cascade=0; }
function cascadeWins(){ cascade=0; wins.forEach(w=>{ const x=70+(cascade*30)%360,y=60+(cascade*28)%240; cascade++; w.style.left=x+'px';w.style.top=y+'px';w.style.zIndex=++z;}); }
function tileWins(){
const n=wins.length; if(!n)return; const cols=Math.ceil(Math.sqrt(n)), rows=Math.ceil(n/cols);
const m=$('.main').getBoundingClientRect(), pad=12;
const w=(m.width-pad*(cols+1))/cols, h=(m.height-pad*(rows+1))/rows;
wins.forEach((el,i)=>{ const r=Math.floor(i/cols),col=i%cols;
el.style.left=(pad+col*(w+pad))+'px'; el.style.top=(pad+r*(h+pad))+'px';
el.style.width=w+'px'; el.style.height=h+'px'; });
}
function drag(el,handle){
let sx,sy,ox,oy,on=false;
handle.addEventListener('mousedown',e=>{ if(e.target.classList.contains('x'))return; on=true; sx=e.clientX;sy=e.clientY;ox=el.offsetLeft;oy=el.offsetTop; e.preventDefault();});
window.addEventListener('mousemove',e=>{ if(!on)return; el.style.left=(ox+e.clientX-sx)+'px'; el.style.top=(oy+e.clientY-sy)+'px';});
window.addEventListener('mouseup',()=>on=false);
}
function resize(el,h){
let sx,sy,ow,oh,on=false;
h.addEventListener('mousedown',e=>{ on=true; sx=e.clientX;sy=e.clientY;ow=el.offsetWidth;oh=el.offsetHeight; e.preventDefault();e.stopPropagation();});
window.addEventListener('mousemove',e=>{ if(!on)return; el.style.width=Math.max(280,ow+e.clientX-sx)+'px'; el.style.height=Math.max(160,oh+e.clientY-sy)+'px';});
window.addEventListener('mouseup',()=>on=false);
}
</script>
</body>
</html>