// 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 */}
{/* 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;