// Battery / Power Source visual component const Battery = ({ source, draw, onChange, onDelete, onPortMouseDown, onCardMouseDown, onCardMouseMove, selected, pendingTarget, period }) => { const { id, name, amount, x, y } = source; const remaining = amount - draw; const pct = amount > 0 ? Math.max(0, Math.min(1, remaining / amount)) : 0; const overload = draw > amount && amount > 0; const empty = amount > 0 && pct < 0.001; const fillColor = overload ? 'var(--red)' : pct < 0.15 ? 'var(--amber)' : pct < 0.4 ? 'var(--green)' : 'var(--cyan)'; const fillDim = overload ? 'var(--red-dim)' : pct < 0.15 ? 'var(--amber-dim)' : pct < 0.4 ? 'var(--green-dim)' : 'var(--cyan-dim)'; // Heartbeat pulse: power sources pulse when carrying load. Faster when overloaded, slower when idle. const isLive = draw > 0 && amount > 0; const pulseClass = !isLive ? '' : overload ? 'pulse-red' : pct < 0.15 ? 'pulse-amber' : pct < 0.4 ? 'pulse-green' : 'pulse-cyan'; // 8 cells in the battery const cells = 8; const filledCells = Math.round(pct * cells); const periodSuffix = period === 'monthly' ? '/mo' : period === 'biweekly' ? '/2wk' : '/wk'; return (
onCardMouseDown(e, id, 'source')} onTouchStart={(e) => onCardMouseDown(e, id, 'source')} > {/* status strip */}
PWR-{String(id).slice(-3).toUpperCase()}
{/* name + bolt icon row */}
onChange(id, { name: e.target.value })} onMouseDown={(e) => e.stopPropagation()} onFocus={(e) => e.target.select()} style={{ background: 'transparent', border: 'none', outline: 'none', color: 'var(--ink-0)', fontSize: 16, fontWeight: 600, width: '100%', padding: '2px 0', borderBottom: '1px dashed transparent' }} onMouseEnter={(e) => e.target.style.borderBottomColor = 'var(--line-2)'} onMouseLeave={(e) => e.target.style.borderBottomColor = 'transparent'} />
{/* battery cells visual */}
{/* scanline effect */} {!empty && !overload && (
)} {Array.from({ length: cells }).map((_, i) => { const active = i < filledCells; return (
); })}
{/* readouts */}
Capacity
$ onChange(id, { amount: parseFloat(e.target.value) || 0 })} onMouseDown={(e) => e.stopPropagation()} onFocus={(e) => e.target.select()} style={{ background: 'transparent', border: 'none', outline: 'none', color: 'var(--ink-0)', fontSize: 18, fontWeight: 700, fontFamily: 'JetBrains Mono, monospace', width: '100%', padding: 0 }} />
Available
${remaining.toFixed(0)}
Draw: ${draw.toFixed(0)}{periodSuffix} {Math.round(pct * 100)}%
{/* output port — right edge */}
{ e.stopPropagation(); onPortMouseDown(e, id); }} onTouchStart={(e) => { e.stopPropagation(); onPortMouseDown(e, id); }} style={{ position: 'absolute', right: -14, top: '50%', transform: 'translateY(-50%)', width: 28, height: 28, borderRadius: '50%', background: 'var(--bg-1)', border: `2px solid ${pendingTarget ? 'var(--cyan)' : 'var(--line-2)'}`, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'crosshair', boxShadow: pendingTarget ? '0 0 16px rgba(76, 243, 255, 0.8)' : `0 0 8px ${overload ? 'var(--red-dim)' : 'var(--cyan-dim)'}`, transition: 'box-shadow 0.2s, border-color 0.2s' }} >
); }; window.Battery = Battery;