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-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 {
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-x-text {
fill: rgba(255, 255, 255, 0.85);
fill: rgba(255, 255, 255, 0.7);
}
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 {
stroke: rgba(255, 255, 255, 0.12);
stroke: rgba(255, 255, 255, 0.10);
}
body.dark .sparkline-svg .cpu-grid-h-line {
stroke: rgba(255, 255, 255, 0.35);
body.dark .sparkline-svg .cpu-tooltip-pill {
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.6));
}

View file

@ -38,10 +38,10 @@ export default function Sparkline({
strokeWidth = 2,
maxPoints = 120,
showGrid = true,
gridColor = 'rgba(0,0,0,0.1)',
fillOpacity = 0.15,
gridColor = 'rgba(0,0,0,0.08)',
fillOpacity = 0.22,
showMarker = true,
markerRadius = 2.8,
markerRadius = 3,
showAxes = false,
yTickStep = 25,
tickCountX = 4,
@ -60,7 +60,10 @@ export default function Sparkline({
const [hoverIdx, setHoverIdx] = useState(-1);
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(() => {
const el = svgRef.current;
@ -211,6 +214,15 @@ export default function Sparkline({
return `${val}${lab ? ' • ' + lab : ''}`;
}, [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 (
<svg
ref={svgRef}
@ -224,9 +236,25 @@ export default function Sparkline({
>
<defs>
<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} />
</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>
{showGrid && (
@ -240,6 +268,7 @@ export default function Sparkline({
y2={g.y2}
stroke={gridColor}
strokeWidth={1}
strokeDasharray="3 5"
className="cpu-grid-line"
/>
))}
@ -252,10 +281,10 @@ export default function Sparkline({
<text
key={`y${i}`}
className="cpu-grid-y-text"
x={Math.max(0, paddingLeft - 4)}
x={Math.max(0, paddingLeft - 6)}
y={tk.y + 4}
textAnchor="end"
fontSize={10}
fontSize={10.5}
>
{tk.label}
</text>
@ -267,7 +296,7 @@ export default function Sparkline({
x={tk.x}
y={paddingTop + drawHeight + 14}
textAnchor="middle"
fontSize={10}
fontSize={10.5}
>
{tk.label}
</text>
@ -283,9 +312,16 @@ export default function Sparkline({
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
filter={`url(#${shadowId})`}
/>
{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] && (
@ -296,16 +332,32 @@ export default function Sparkline({
x2={pointsArr[hoverIdx][0]}
y1={paddingTop}
y2={paddingTop + drawHeight}
stroke="rgba(0,0,0,0.2)"
stroke={stroke}
strokeOpacity={0.45}
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
className="cpu-grid-text"
x={pointsArr[hoverIdx][0]}
y={paddingTop + 12}
className="cpu-tooltip-text"
x={tooltipX + tooltipPillWidth / 2}
y={paddingTop + 14}
textAnchor="middle"
fontSize={11}
fontWeight={600}
fill="#fff"
>
{hoverText}
</text>

View file

@ -8,11 +8,29 @@
}
.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 {
margin-bottom: 10px;
font-size: 11px;
margin-bottom: 12px;
font-size: 11.5px;
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 {
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 === '%') {
return (v) => `${Number(v).toFixed(1)}%`;

View file

@ -29,6 +29,8 @@
.obs-stamp {
opacity: 0.7;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 11.5px;
}
.obs-dot {
@ -38,16 +40,30 @@
border-radius: 50%;
margin-right: 6px;
vertical-align: middle;
box-shadow: 0 0 0 3px rgba(82, 196, 26, 0.18);
}
.obs-dot.is-alive {
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 {
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 {
opacity: 0.7;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}