feat: live stacks in hand viewer + retheme UI to RTO black/orange palette

Hand viewer:
- stacks now decrement as players commit chips (street-aware "to"-amount
  accounting), showing e.g. 300 -> 285 after a 15 open, "all in" at 0; pot is
  computed from total committed (accurate, no double-counting raises)

Theme (match the rec-theory-optimal look — warm black & orange, not Halloween):
- deep near-black bg (#070707 / #0e0e0e panels), warm orange accent (#ff7a00),
  amber-gold secondary (#ffb347), muted green (#8fd694); warm dark borders
- killed the neon-orange glows and the purple accents; chat app + all standalone
  pages (logs/self/journal/hand/recap/hands) on one palette

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 00:53:18 +00:00
parent 7b65f81d7e
commit 4882225751
7 changed files with 1069 additions and 1045 deletions
+41 -17
View File
@@ -3,13 +3,13 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0b0d12" />
<meta name="theme-color" content="#070707" />
<title>Lyra — Hand</title>
<style>
:root {
--bg:#0b0d12; --bg-elev:#141821; --border:#232936; --text:#e6e9ef;
--fade:#8b93a7; --accent:#7aa2ff; --felt:#16322a; --feltline:#0f5132;
--chip:#ffcf6b; --hero:#7aa2ff;
--bg:#070707; --bg-elev:#0e0e0e; --border:#2a1d12; --text:#e8e8e8;
--fade:#8a8a8a; --accent:#ff7a00; --felt:#16322a; --feltline:#0f5132;
--chip:#ffb347; --hero:#ff7a00;
}
*{box-sizing:border-box;}
html,body{margin:0;min-height:100%;background:var(--bg);color:var(--text);
@@ -42,8 +42,8 @@
.seat{position:absolute;transform:translate(-50%,-50%);width:96px;text-align:center;
background:rgba(13,16,22,.85);border:1px solid var(--border);border-radius:10px;padding:5px 4px;}
.seat.hero{border-color:var(--hero);box-shadow:0 0 10px rgba(122,162,255,.4);}
.seat.acting{border-color:var(--chip);box-shadow:0 0 12px rgba(255,207,107,.6);}
.seat.hero{border-color:var(--hero);box-shadow:0 0 10px rgba(255,122,0,.4);}
.seat.acting{border-color:var(--chip);box-shadow:0 0 12px rgba(255,179,71,.6);}
.seat .pos{font-size:.66rem;color:var(--accent);font-weight:700;letter-spacing:.4px;}
.seat .nm{font-size:.66rem;color:var(--fade);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.seat .cards{display:flex;gap:3px;justify-content:center;margin:3px 0;}
@@ -52,7 +52,7 @@
.seat.folded{opacity:.4;}
.controls{display:flex;gap:8px;align-items:center;justify-content:center;margin:14px 0 6px;}
.controls button{background:#1b2333;border:1px solid var(--border);color:var(--text);
.controls button{background:#241400;border:1px solid var(--border);color:var(--text);
border-radius:8px;padding:8px 14px;font-size:.95rem;cursor:pointer;-webkit-tap-highlight-color:transparent;}
.controls button:disabled{opacity:.4;}
.step-label{color:var(--fade);font-size:.8rem;min-width:80px;text-align:center;}
@@ -60,13 +60,13 @@
.log{margin-top:14px;border-top:1px solid var(--border);padding-top:10px;}
.log .ln{padding:5px 8px;border-radius:6px;font-size:.9rem;display:flex;gap:8px;}
.log .ln.cur{background:#1b2333;}
.log .ln.cur{background:#241400;}
.log .ln.brd{color:var(--fade);font-style:italic;}
.log .st{color:var(--fade);font-size:.72rem;width:54px;flex:none;text-transform:uppercase;}
.summary{margin-top:14px;background:var(--bg-elev);border:1px solid var(--border);border-radius:10px;padding:12px;}
.summary .lbl{color:var(--fade);font-size:.72rem;text-transform:uppercase;letter-spacing:.5px;}
.err{color:#ff6b6b;text-align:center;padding:40px;}
.net-pos{color:#5ad1a0;} .net-neg{color:#ff6b6b;}
.net-pos{color:#8fd694;} .net-neg{color:#ff6b6b;}
</style>
</head>
<body>
@@ -140,7 +140,9 @@
// place seats around the oval
const seatsEl = document.getElementById('seats');
const starts = {};
ordered.forEach((p,i)=>{
starts[p.pos] = (p.stack!=null ? Number(p.stack) : null);
const ang = (90 + i*(360/n)) * Math.PI/180; // bottom = 90deg
const x = 50 + 46*Math.cos(ang), y = 50 + 44*Math.sin(ang);
const el = document.createElement('div');
@@ -151,7 +153,7 @@
el.innerHTML = `<div class="pos">${esc(p.pos||'')}</div>`
+ (p.name?`<div class="nm">${esc(p.name)}</div>`:'')
+ `<div class="cards">${hcards?cards(hcards,true):'<span class="card sm back">x</span><span class="card sm back">x</span>'}</div>`
+ (p.stack!=null?`<div class="stack">${esc(p.stack)}</div>`:'')
+ `<div class="stack" data-stack>${p.stack!=null?esc(p.stack):''}</div>`
+ `<div class="act" data-act></div>`;
seatsEl.appendChild(el);
});
@@ -167,25 +169,47 @@
return `<div class="ln" data-i="${idx}"><span class="st">${esc(a.street||'')}</span>${esc(a.pos||'')} ${esc(a.action||'')}${amt}</div>`;
}).join('');
const cap = s => s ? s[0].toUpperCase()+s.slice(1) : s;
const fmt = n => Number.isInteger(n) ? n : Math.round(n*100)/100;
function draw(){
let pot = 0, board = [], street = 'Preflop';
let board = [], street = 'Preflop';
const lastAct = {}, folded = {};
// street-aware chip accounting: amounts are "to" totals for the street
const contrib = {}; // committed in prior (flushed) streets
let streetCommit = {}, streetBet = 0, curStreet = 'preflop';
const flushStreet = () => { for(const p in streetCommit){ contrib[p]=(contrib[p]||0)+streetCommit[p]; } streetCommit={}; streetBet=0; };
for(let i=0;i<step;i++){
const a = acts[i];
if(a.board){ board = a.board; street = a.street; continue; }
if(a.street) street = a.street;
if(a.amount!=null && ['call','bet','raise','allin','post'].includes(a.action)) pot += Number(a.amount)||0;
if(a.pos){ lastAct[a.pos] = (a.action||'') + (a.amount!=null?' '+a.amount:''); }
if(a.action==='fold' && a.pos) folded[a.pos]=true;
if(a.board){ flushStreet(); curStreet=a.street; board=a.board; street=cap(a.street); continue; }
if(a.street && a.street!==curStreet){ flushStreet(); curStreet=a.street; }
if(a.street) street = cap(a.street);
const pos=a.pos, amt=(a.amount!=null?Number(a.amount):null);
if(pos){
switch(a.action){
case 'post': case 'bet': streetCommit[pos]=amt||0; streetBet=Math.max(streetBet, amt||0); break;
case 'raise': case 'allin': streetCommit[pos]=(amt!=null?amt:streetBet); streetBet=Math.max(streetBet, streetCommit[pos]); break;
case 'call': streetCommit[pos]=(amt!=null?amt:streetBet); break;
case 'fold': folded[pos]=true; break;
}
lastAct[pos]=(a.action||'')+(amt!=null?' '+amt:'');
}
}
// committed total per player (flushed streets + current street), pot = sum
const committed={}, allPos=new Set([...Object.keys(contrib),...Object.keys(streetCommit)]);
let pot=0;
allPos.forEach(p=>{ committed[p]=(contrib[p]||0)+(streetCommit[p]||0); pot+=committed[p]; });
boardEl.innerHTML = cards(board);
potEl.textContent = pot ? ('Pot ~'+pot) : '';
potEl.textContent = pot ? ('Pot '+fmt(pot)) : '';
streetEl.textContent = street;
document.querySelectorAll('.seat').forEach(s=>{
const pos=s.dataset.pos;
s.querySelector('[data-act]').textContent = lastAct[pos]||'';
s.classList.toggle('folded', !!folded[pos]);
s.classList.remove('acting');
const stEl=s.querySelector('[data-stack]'), start=starts[pos], c=committed[pos]||0;
if(start!=null){ const rem=start-c; stEl.textContent = rem<=0 ? 'all in' : fmt(rem); }
else { stEl.textContent = c ? ''+fmt(c) : ''; }
});
const cur = acts[step-1];
if(cur && cur.pos){