Redesign dashboard with steampunk neon theme and live gauges.

Ambient background, 3D neon cards, brass HUD styling, glowing charts, gauge rings, and refreshed Command Deck, Fleet Roster, Forge, and Calibrate pages.
This commit is contained in:
drjones
2026-05-27 00:43:34 -07:00
parent 6db9a33a20
commit 9d223b8137
20 changed files with 1638 additions and 307 deletions

View File

@@ -7,6 +7,7 @@ import {
XAxis,
YAxis,
} from 'recharts';
import './HashrateChart.css';
export interface ChartPoint {
time: string;
@@ -19,48 +20,149 @@ interface HashrateChartProps {
title?: string;
color?: string;
unit?: string;
height?: number;
}
export default function HashrateChart({ data, title, color = '#06b6d4', unit = 'H/s' }: HashrateChartProps) {
const GRAD_IDS = ['cyan', 'magenta', 'amber', 'green', 'purple'] as const;
function colorToId(color: string): string {
if (color.includes('f5ff') || color.includes('06b6d4') || color === '#00f5ff') return 'cyan';
if (color.includes('2da6') || color.includes('8b5cf6')) return 'magenta';
if (color.includes('b020') || color.includes('eab308')) return 'amber';
if (color.includes('39ff') || color.includes('22c55e')) return 'green';
return 'purple';
}
export default function HashrateChart({
data,
title,
color = '#00f5ff',
unit = 'H/s',
height = 280,
}: HashrateChartProps) {
const gradId = colorToId(color);
if (data.length === 0) {
return (
<div className="chart-empty card">
<p>{title || 'Chart'} waiting for data...</p>
<div className="chart-empty neon-chart-panel">
<div className="chart-empty-icon"></div>
<p className="font-tech">{title || 'Telemetry'}</p>
<span>Awaiting signal from fleet...</span>
</div>
);
}
return (
<div className="chart-wrap">
{title && <h3 className="chart-title">{title}</h3>}
<ResponsiveContainer width="100%" height={260}>
<AreaChart data={data}>
<div className="chart-wrap neon-chart-panel">
{title && (
<div className="chart-header">
<h3 className="chart-title font-display">{title}</h3>
<span className="chart-live pulse"> LIVE</span>
</div>
)}
<ResponsiveContainer width="100%" height={height}>
<AreaChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
<defs>
<linearGradient id={`grad-${color}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={color} stopOpacity={0.35} />
<stop offset="95%" stopColor={color} stopOpacity={0} />
<linearGradient id={`area-fill-${gradId}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={color} stopOpacity={0.55} />
<stop offset="50%" stopColor={color} stopOpacity={0.15} />
<stop offset="100%" stopColor={color} stopOpacity={0} />
</linearGradient>
<filter id={`glow-${gradId}`}>
<feGaussianBlur stdDeviation="3" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#243049" />
<XAxis dataKey="time" stroke="#64748b" fontSize={11} tickLine={false} />
<YAxis stroke="#64748b" fontSize={11} tickLine={false} tickFormatter={(v) => formatShort(v, unit)} />
<Tooltip
contentStyle={{ background: '#111827', border: '1px solid #2a3a5c', borderRadius: 8 }}
labelStyle={{ color: '#94a3b8' }}
formatter={(value: number) => [formatShort(value, unit), title || 'Value']}
<CartesianGrid strokeDasharray="4 8" stroke="rgba(201, 162, 39, 0.12)" vertical={false} />
<XAxis
dataKey="time"
stroke="rgba(196, 181, 160, 0.5)"
fontSize={10}
fontFamily="Orbitron, monospace"
tickLine={false}
axisLine={{ stroke: 'rgba(201, 162, 39, 0.25)' }}
/>
<YAxis
stroke="rgba(196, 181, 160, 0.5)"
fontSize={10}
fontFamily="Orbitron, monospace"
tickLine={false}
axisLine={false}
tickFormatter={(v) => formatShort(v, unit)}
/>
<Tooltip
content={(props) => (
<NeonTooltip
active={props.active}
payload={props.payload as { value: number }[] | undefined}
label={props.label as string | undefined}
unit={unit}
title={title}
color={color}
/>
)}
/>
<Area
type="monotone"
dataKey="value"
stroke={color}
strokeWidth={2.5}
fill={`url(#area-fill-${gradId})`}
filter={`url(#glow-${gradId})`}
animationDuration={800}
animationEasing="ease-out"
/>
<Area type="monotone" dataKey="value" stroke={color} fill={`url(#grad-${color})`} strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</div>
);
}
function NeonTooltip({
active,
payload,
label,
unit,
title,
color,
}: {
active?: boolean;
payload?: { value: number }[];
label?: string;
unit: string;
title?: string;
color: string;
}) {
if (!active || !payload?.length) return null;
const v = payload[0].value;
return (
<div className="neon-tooltip">
<div className="neon-tooltip-time font-tech">{label}</div>
<div className="neon-tooltip-value" style={{ color, textShadow: `0 0 12px ${color}` }}>
{formatFull(v, unit)}
</div>
<div className="neon-tooltip-label">{title || 'Reading'}</div>
</div>
);
}
function formatShort(v: number, unit: string): string {
if (unit === 'H/s') {
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
if (v >= 1_000) return `${(v / 1_000).toFixed(1)}K`;
return `${v.toFixed(0)}`;
}
return `${v.toFixed(1)}${unit === '%' ? '%' : ''}`;
return `${v.toFixed(0)}${unit === '%' ? '%' : ''}`;
}
function formatFull(v: number, unit: string): string {
if (unit === 'H/s') {
if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(3)} MH/s`;
if (v >= 1_000) return `${(v / 1_000).toFixed(2)} KH/s`;
return `${v.toFixed(0)} H/s`;
}
return `${v.toFixed(1)}${unit}`;
}