From eb1f788033c696c1077002048f144b0bfd59592b Mon Sep 17 00:00:00 2001 From: Fringg Date: Wed, 25 Feb 2026 10:07:49 +0300 Subject: [PATCH] feat: add TrafficProgressBar and Sparkline components TrafficProgressBar with zone tints, shimmer animation, glow dot, threshold markers, scale labels, unlimited/compact modes, and full ARIA support. Sparkline SVG component ready for daily usage data. --- src/components/dashboard/Sparkline.tsx | 63 +++++++ .../dashboard/TrafficProgressBar.tsx | 157 ++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 src/components/dashboard/Sparkline.tsx create mode 100644 src/components/dashboard/TrafficProgressBar.tsx diff --git a/src/components/dashboard/Sparkline.tsx b/src/components/dashboard/Sparkline.tsx new file mode 100644 index 0000000..e85e948 --- /dev/null +++ b/src/components/dashboard/Sparkline.tsx @@ -0,0 +1,63 @@ +import { useId } from 'react'; + +interface SparklineProps { + data: number[]; + width?: number; + height?: number; + className?: string; +} + +export default function Sparkline({ + data, + width = 120, + height = 32, + className = '', +}: SparklineProps) { + const gradientId = useId(); + + if (data.length < 2) return null; + + const max = Math.max(...data); + const min = Math.min(...data); + const range = max - min || 1; + const padding = 2; + const innerW = width - padding * 2; + const innerH = height - padding * 2; + + const points = data.map((v, i) => { + const x = padding + (i / (data.length - 1)) * innerW; + const y = padding + innerH - ((v - min) / range) * innerH; + return `${x},${y}`; + }); + + const polyline = points.join(' '); + const lastX = padding + innerW; + const bottomY = padding + innerH; + const areaPath = `M${points[0]} ${points.slice(1).join(' ')} L${lastX},${bottomY} L${padding},${bottomY} Z`; + + return ( + + ); +} diff --git a/src/components/dashboard/TrafficProgressBar.tsx b/src/components/dashboard/TrafficProgressBar.tsx new file mode 100644 index 0000000..073dafc --- /dev/null +++ b/src/components/dashboard/TrafficProgressBar.tsx @@ -0,0 +1,157 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { getTrafficZone } from '../../utils/trafficZone'; + +interface TrafficProgressBarProps { + usedGb: number; + limitGb: number; + percent: number; + isUnlimited: boolean; + showScale?: boolean; + showThresholds?: boolean; + compact?: boolean; +} + +const THRESHOLDS = [50, 75, 90]; + +export default function TrafficProgressBar({ + limitGb, + percent, + isUnlimited, + showScale = false, + showThresholds = false, + compact = false, +}: TrafficProgressBarProps) { + const { t } = useTranslation(); + const zone = useMemo(() => getTrafficZone(percent), [percent]); + const clampedPercent = Math.min(percent, 100); + const barHeight = compact ? 'h-2' : 'h-3'; + + if (isUnlimited) { + return ( +
+
+
+
+ {!compact && ( + + ); + } + + return ( +
+ {/* Track with zone tint backgrounds */} +
+ {/* Zone tint backgrounds */} + {showThresholds && ( + <> +
+
+
+ + )} + + {/* Fill bar */} +
+ {/* Shimmer overlay */} + + + {/* Glow dot at fill edge */} + {clampedPercent > 2 && !compact && ( + + ); +}