style(frontend): prettier charts, drop redundant frame, format net rates

- Sparkline: multi-stop gradient fill, soft drop-shadow under the line,
  dashed grid, glowing pulse on the latest-point marker, pill-shaped
  tooltip with dashed crosshair
- XrayMetricsModal: glow + pulse on the observatory alive dot,
  monospace stamps/listen text
- SystemHistoryModal: keep just the modal's frame around the chart (the
  inner wrapper I'd added stacked a second border on top); strip the
  decimal from Net Up/Down (25.63 KB/s → 25 KB/s) only on this chart's
  formatter
This commit is contained in:
MHSanaei 2026-05-22 04:07:22 +02:00
parent cb45febdc2
commit f9fb197cdb
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
5 changed files with 123 additions and 23 deletions

View file

@ -5,16 +5,30 @@
.sparkline-svg .cpu-grid-y-text, .sparkline-svg .cpu-grid-y-text,
.sparkline-svg .cpu-grid-x-text { .sparkline-svg .cpu-grid-x-text {
fill: rgba(0, 0, 0, 0.65); fill: rgba(0, 0, 0, 0.55);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
letter-spacing: 0.2px;
} }
.sparkline-svg .cpu-grid-text { .sparkline-svg .cpu-grid-text {
fill: rgba(0, 0, 0, 0.88); fill: rgba(0, 0, 0, 0.88);
} }
.sparkline-svg .cpu-grid-line {
stroke: rgba(0, 0, 0, 0.08);
}
.sparkline-svg .cpu-tooltip-text {
pointer-events: none;
}
.sparkline-svg .cpu-tooltip-pill {
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.18));
}
body.dark .sparkline-svg .cpu-grid-y-text, body.dark .sparkline-svg .cpu-grid-y-text,
body.dark .sparkline-svg .cpu-grid-x-text { body.dark .sparkline-svg .cpu-grid-x-text {
fill: rgba(255, 255, 255, 0.85); fill: rgba(255, 255, 255, 0.7);
} }
body.dark .sparkline-svg .cpu-grid-text { body.dark .sparkline-svg .cpu-grid-text {
@ -22,9 +36,9 @@ body.dark .sparkline-svg .cpu-grid-text {
} }
body.dark .sparkline-svg .cpu-grid-line { body.dark .sparkline-svg .cpu-grid-line {
stroke: rgba(255, 255, 255, 0.12); stroke: rgba(255, 255, 255, 0.10);
} }
body.dark .sparkline-svg .cpu-grid-h-line { body.dark .sparkline-svg .cpu-tooltip-pill {
stroke: rgba(255, 255, 255, 0.35); filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.6));
} }

View file

@ -38,10 +38,10 @@ export default function Sparkline({
strokeWidth = 2, strokeWidth = 2,
maxPoints = 120, maxPoints = 120,
showGrid = true, showGrid = true,
gridColor = 'rgba(0,0,0,0.1)', gridColor = 'rgba(0,0,0,0.08)',
fillOpacity = 0.15, fillOpacity = 0.22,
showMarker = true, showMarker = true,
markerRadius = 2.8, markerRadius = 3,
showAxes = false, showAxes = false,
yTickStep = 25, yTickStep = 25,
tickCountX = 4, tickCountX = 4,
@ -60,7 +60,10 @@ export default function Sparkline({
const [hoverIdx, setHoverIdx] = useState(-1); const [hoverIdx, setHoverIdx] = useState(-1);
const reactId = useId(); const reactId = useId();
const gradId = `spkGrad-${reactId.replace(/[^a-zA-Z0-9]/g, '')}`; const safeId = reactId.replace(/[^a-zA-Z0-9]/g, '');
const gradId = `spkGrad-${safeId}`;
const shadowId = `spkShadow-${safeId}`;
const glowId = `spkGlow-${safeId}`;
useEffect(() => { useEffect(() => {
const el = svgRef.current; const el = svgRef.current;
@ -211,6 +214,15 @@ export default function Sparkline({
return `${val}${lab ? ' • ' + lab : ''}`; return `${val}${lab ? ' • ' + lab : ''}`;
}, [hoverIdx, dataSlice, labelsSlice, tooltipFormatter, yFormatter]); }, [hoverIdx, dataSlice, labelsSlice, tooltipFormatter, yFormatter]);
const tooltipPillWidth = Math.max(48, hoverText.length * 6.2 + 14);
const hoverPoint = hoverIdx >= 0 ? pointsArr[hoverIdx] : null;
const tooltipX = hoverPoint
? Math.max(
paddingLeft + 2,
Math.min(effectiveVbWidth - paddingRight - tooltipPillWidth - 2, hoverPoint[0] - tooltipPillWidth / 2),
)
: 0;
return ( return (
<svg <svg
ref={svgRef} ref={svgRef}
@ -224,9 +236,25 @@ export default function Sparkline({
> >
<defs> <defs>
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1"> <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={stroke} stopOpacity={fillOpacity} /> <stop offset="0%" stopColor={stroke} stopOpacity={Math.min(1, fillOpacity * 1.8)} />
<stop offset="50%" stopColor={stroke} stopOpacity={fillOpacity * 0.7} />
<stop offset="100%" stopColor={stroke} stopOpacity={0} /> <stop offset="100%" stopColor={stroke} stopOpacity={0} />
</linearGradient> </linearGradient>
<filter id={shadowId} x="-10%" y="-50%" width="120%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2.4" />
<feOffset dx="0" dy="2" result="offsetBlur" />
<feComponentTransfer>
<feFuncA type="linear" slope="0.45" />
</feComponentTransfer>
<feMerge>
<feMergeNode />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<radialGradient id={glowId}>
<stop offset="0%" stopColor={stroke} stopOpacity="0.55" />
<stop offset="100%" stopColor={stroke} stopOpacity="0" />
</radialGradient>
</defs> </defs>
{showGrid && ( {showGrid && (
@ -240,6 +268,7 @@ export default function Sparkline({
y2={g.y2} y2={g.y2}
stroke={gridColor} stroke={gridColor}
strokeWidth={1} strokeWidth={1}
strokeDasharray="3 5"
className="cpu-grid-line" className="cpu-grid-line"
/> />
))} ))}
@ -252,10 +281,10 @@ export default function Sparkline({
<text <text
key={`y${i}`} key={`y${i}`}
className="cpu-grid-y-text" className="cpu-grid-y-text"
x={Math.max(0, paddingLeft - 4)} x={Math.max(0, paddingLeft - 6)}
y={tk.y + 4} y={tk.y + 4}
textAnchor="end" textAnchor="end"
fontSize={10} fontSize={10.5}
> >
{tk.label} {tk.label}
</text> </text>
@ -267,7 +296,7 @@ export default function Sparkline({
x={tk.x} x={tk.x}
y={paddingTop + drawHeight + 14} y={paddingTop + drawHeight + 14}
textAnchor="middle" textAnchor="middle"
fontSize={10} fontSize={10.5}
> >
{tk.label} {tk.label}
</text> </text>
@ -283,9 +312,16 @@ export default function Sparkline({
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
filter={`url(#${shadowId})`}
/> />
{showMarker && lastPoint && ( {showMarker && lastPoint && (
<circle cx={lastPoint[0]} cy={lastPoint[1]} r={markerRadius} fill={stroke} /> <>
<circle cx={lastPoint[0]} cy={lastPoint[1]} r={markerRadius * 3} fill={`url(#${glowId})`}>
<animate attributeName="r" values={`${markerRadius * 2.4};${markerRadius * 3.4};${markerRadius * 2.4}`} dur="2.6s" repeatCount="indefinite" />
</circle>
<circle cx={lastPoint[0]} cy={lastPoint[1]} r={markerRadius + 1.5} fill={stroke} fillOpacity={0.25} />
<circle cx={lastPoint[0]} cy={lastPoint[1]} r={markerRadius} fill={stroke} stroke="#fff" strokeWidth={1.5} />
</>
)} )}
{showTooltip && hoverIdx >= 0 && pointsArr[hoverIdx] && ( {showTooltip && hoverIdx >= 0 && pointsArr[hoverIdx] && (
@ -296,16 +332,32 @@ export default function Sparkline({
x2={pointsArr[hoverIdx][0]} x2={pointsArr[hoverIdx][0]}
y1={paddingTop} y1={paddingTop}
y2={paddingTop + drawHeight} y2={paddingTop + drawHeight}
stroke="rgba(0,0,0,0.2)" stroke={stroke}
strokeOpacity={0.45}
strokeWidth={1} strokeWidth={1}
strokeDasharray="3 4"
/>
<circle cx={pointsArr[hoverIdx][0]} cy={pointsArr[hoverIdx][1]} r={5} fill={stroke} fillOpacity={0.25} />
<circle cx={pointsArr[hoverIdx][0]} cy={pointsArr[hoverIdx][1]} r={3.5} fill={stroke} stroke="#fff" strokeWidth={1.5} />
<rect
x={tooltipX}
y={paddingTop + 2}
width={tooltipPillWidth}
height={18}
rx={9}
ry={9}
className="cpu-tooltip-pill"
fill={stroke}
fillOpacity={0.92}
/> />
<circle cx={pointsArr[hoverIdx][0]} cy={pointsArr[hoverIdx][1]} r={3.5} fill={stroke} />
<text <text
className="cpu-grid-text" className="cpu-tooltip-text"
x={pointsArr[hoverIdx][0]} x={tooltipX + tooltipPillWidth / 2}
y={paddingTop + 12} y={paddingTop + 14}
textAnchor="middle" textAnchor="middle"
fontSize={11} fontSize={11}
fontWeight={600}
fill="#fff"
> >
{hoverText} {hoverText}
</text> </text>

View file

@ -8,11 +8,29 @@
} }
.cpu-chart-wrap { .cpu-chart-wrap {
padding: 8px 16px 16px; margin: 8px 8px 16px;
padding: 16px 18px 18px;
border-radius: 14px;
background: linear-gradient(180deg, rgba(99, 102, 241, 0.05), rgba(99, 102, 241, 0));
border: 1px solid rgba(99, 102, 241, 0.12);
box-shadow: 0 2px 12px rgba(99, 102, 241, 0.06);
}
body.dark .cpu-chart-wrap {
background: linear-gradient(180deg, rgba(129, 140, 248, 0.08), rgba(129, 140, 248, 0));
border-color: rgba(129, 140, 248, 0.16);
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.25);
}
html[data-theme='ultra-dark'] .cpu-chart-wrap {
background: linear-gradient(180deg, rgba(129, 140, 248, 0.05), rgba(129, 140, 248, 0));
border-color: rgba(129, 140, 248, 0.10);
} }
.cpu-chart-meta { .cpu-chart-meta {
margin-bottom: 10px; margin-bottom: 12px;
font-size: 11px; font-size: 11.5px;
opacity: 0.65; opacity: 0.65;
letter-spacing: 0.3px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
} }

View file

@ -35,7 +35,7 @@ const METRICS: MetricDef[] = [
function unitFormatter(unit: string, activeKey: string): (v: number) => string { function unitFormatter(unit: string, activeKey: string): (v: number) => string {
if (unit === 'B/s') { if (unit === 'B/s') {
return (v) => `${SizeFormatter.sizeFormat(Math.max(0, Number(v) || 0))}/s`; return (v) => `${SizeFormatter.sizeFormat(Math.max(0, Number(v) || 0)).replace(/\.\d+/, '')}/s`;
} }
if (unit === '%') { if (unit === '%') {
return (v) => `${Number(v).toFixed(1)}%`; return (v) => `${Number(v).toFixed(1)}%`;

View file

@ -29,6 +29,8 @@
.obs-stamp { .obs-stamp {
opacity: 0.7; opacity: 0.7;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11.5px;
} }
.obs-dot { .obs-dot {
@ -38,16 +40,30 @@
border-radius: 50%; border-radius: 50%;
margin-right: 6px; margin-right: 6px;
vertical-align: middle; vertical-align: middle;
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.18);
} }
.obs-dot.is-alive { .obs-dot.is-alive {
background: #52c41a; background: #52c41a;
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.22);
animation: obs-dot-pulse 2.2s ease-in-out infinite;
} }
.obs-dot.is-dead { .obs-dot.is-dead {
background: #f5222d; background: #f5222d;
box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.22);
}
@keyframes obs-dot-pulse {
0%, 100% { box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.22); }
50% { box-shadow: 0 0 0 6px rgba(82, 196, 26, 0.06); }
}
@media (prefers-reduced-motion: reduce) {
.obs-dot.is-alive { animation: none; }
} }
.listen-tag { .listen-tag {
opacity: 0.7; opacity: 0.7;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
} }