mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 05:04:22 +00:00
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:
parent
cb45febdc2
commit
f9fb197cdb
5 changed files with 123 additions and 23 deletions
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)}%`;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue