@@ -16,27 +16,20 @@ export function formatSpeed(speedBps: number): string {
1616 }
1717}
1818
19- // ─── Circular segmented ring (mobile only) ───────────────────────────────────
20- //
21- // 30 thin arc segments arranged clockwise from the top (12 o'clock).
22- // Each segment occupies (360 / 30) = 12° with a small gap between them.
23- // Fill logic mirrors the horizontal bar: full segments + one partial segment.
24-
25- const SEGMENT_COUNT = 30
19+ const SEGMENT_COUNT = 18
2620const SEGMENT_KEYS = Array . from (
2721 { length : SEGMENT_COUNT } ,
2822 ( _ , i ) => `segment-${ i } `
2923)
30- const SEGMENT_ANGLE = 360 / SEGMENT_COUNT // 12° per segment
31- const GAP_ANGLE = 2.2 // degrees of gap on each side
24+ const SEGMENT_ANGLE = 360 / SEGMENT_COUNT
25+ const GAP_ANGLE = 2.5
3226const ARC_ANGLE = SEGMENT_ANGLE - GAP_ANGLE * 2
3327
34- const RING_SIZE = 200 // SVG viewBox size (px)
35- const CENTER = RING_SIZE / 2 // 100
36- const RADIUS = 84 // arc radius
37- const STROKE_WIDTH = 4.5 // thinner stroke for a sleeker look
28+ const RING_SIZE = 200
29+ const CENTER = RING_SIZE / 2
30+ const RADIUS = 84
31+ const STROKE_WIDTH = 4.5
3832
39- /** Convert polar coordinates (angle from 12 o'clock, clockwise) to Cartesian. */
4033function polarToCartesian ( cx : number , cy : number , r : number , angleDeg : number ) {
4134 const angleRad = ( ( angleDeg - 90 ) * Math . PI ) / 180
4235 return {
@@ -45,7 +38,6 @@ function polarToCartesian(cx: number, cy: number, r: number, angleDeg: number) {
4538 }
4639}
4740
48- /** Build an SVG arc path for a segment starting at `startAngle` spanning `sweep` degrees. */
4941function arcPath ( startAngle : number , sweep : number ) : string {
5042 const start = polarToCartesian ( CENTER , CENTER , RADIUS , startAngle )
5143 const end = polarToCartesian ( CENTER , CENTER , RADIUS , startAngle + sweep )
@@ -61,9 +53,6 @@ function CircularRing({ percentage }: CircularRingProps) {
6153 const { t } = useTranslation ( )
6254 const filledSegments = Math . floor ( ( percentage / 100 ) * SEGMENT_COUNT )
6355
64- // How far into the current (partial) segment we are, as a 0–1 fraction
65- const partialFraction = ( percentage / 100 ) * SEGMENT_COUNT - filledSegments
66-
6756 return (
6857 < svg
6958 viewBox = { `0 0 ${ RING_SIZE } ${ RING_SIZE } ` }
@@ -78,37 +67,50 @@ function CircularRing({ percentage }: CircularRingProps) {
7867 >
7968 { SEGMENT_KEYS . map ( ( segmentKey , index ) => {
8069 const segmentStartAngle = index * SEGMENT_ANGLE + GAP_ANGLE
81-
8270 const isFilled = index < filledSegments
83- const isPartial = index === filledSegments && partialFraction > 0
71+ const isPartiallyFilled =
72+ index === filledSegments &&
73+ percentage % ( 100 / SEGMENT_COUNT ) > 0
8474
85- // For the partial segment we shorten the visible arc proportionally
86- const visibleSweep = isFilled
87- ? ARC_ANGLE
88- : isPartial
89- ? ARC_ANGLE * partialFraction
90- : 0
75+ let fillFraction = 0
76+ if ( isFilled ) {
77+ fillFraction = 1
78+ } else if ( isPartiallyFilled ) {
79+ fillFraction =
80+ ( percentage % ( 100 / SEGMENT_COUNT ) ) / ( 100 / SEGMENT_COUNT )
81+ }
82+
83+ const arcD = arcPath ( segmentStartAngle , ARC_ANGLE )
9184
9285 return (
9386 < g key = { segmentKey } >
94- { /* Background (unfilled) arc */ }
9587 < path
96- d = { arcPath ( segmentStartAngle , ARC_ANGLE ) }
88+ d = { arcD }
9789 fill = "none"
9890 stroke = "var(--input)"
9991 strokeWidth = { STROKE_WIDTH }
100- strokeLinecap = "round"
101- className = "transition-all duration-300 ease-in-out"
92+ strokeLinecap = "butt"
10293 />
103- { /* Filled arc (rendered on top) */ }
104- { visibleSweep > 0 && (
94+ { isFilled && (
10595 < path
106- d = { arcPath ( segmentStartAngle , visibleSweep ) }
96+ d = { arcD }
10797 fill = "none"
10898 stroke = "var(--app-primary)"
10999 strokeWidth = { STROKE_WIDTH }
110- strokeLinecap = "round"
111- className = "transition-all duration-300 ease-in-out"
100+ strokeLinecap = "butt"
101+ />
102+ ) }
103+ { isPartiallyFilled && (
104+ < path
105+ d = { arcD }
106+ fill = "none"
107+ stroke = "var(--app-primary)"
108+ strokeWidth = { STROKE_WIDTH }
109+ strokeLinecap = "butt"
110+ pathLength = { 100 }
111+ strokeDasharray = "100"
112+ strokeDashoffset = { 100 - fillFraction * 100 }
113+ className = "transition-[stroke-dashoffset] duration-300 ease-in-out"
112114 />
113115 ) }
114116 </ g >
@@ -118,8 +120,6 @@ function CircularRing({ percentage }: CircularRingProps) {
118120 )
119121}
120122
121- // ─── Main component ───────────────────────────────────────────────────────────
122-
123123export function TransferProgressBar ( { progress } : TransferProgressBarProps ) {
124124 const { percentage } = progress
125125 const barCount = 30
@@ -128,12 +128,10 @@ export function TransferProgressBar({ progress }: TransferProgressBarProps) {
128128
129129 return (
130130 < div className = "space-y-3" >
131- { /* ── Mobile layout: circular ring ── */ }
132131 < div className = "sm:hidden flex flex-col items-center gap-3" >
133132 < div className = "relative inline-flex items-center justify-center" >
134133 < CircularRing percentage = { percentage } />
135134
136- { /* Labels centred inside the ring */ }
137135 < div className = "absolute inset-0 flex flex-col items-center justify-center gap-0.5 text-center pointer-events-none" >
138136 < span className = "text-2xl font-normal leading-none tabular-nums" >
139137 { percentage . toFixed ( 1 ) } %
@@ -154,7 +152,6 @@ export function TransferProgressBar({ progress }: TransferProgressBarProps) {
154152 </ div >
155153 </ div >
156154
157- { /* ── Desktop layout: horizontal segment bars ── */ }
158155 < div className = "hidden sm:block space-y-2" >
159156 < div className = "flex items-center justify-between text-xs" >
160157 < span > { t ( 'common:transfer.progress' ) } </ span >
@@ -178,7 +175,6 @@ export function TransferProgressBar({ progress }: TransferProgressBarProps) {
178175
179176 return (
180177 < div
181- // biome-ignore lint/suspicious/noArrayIndexKey: The values are always static so it is okay
182178 key = { index }
183179 className = "relative flex-1 rounded-sm bg-input transition-all duration-300 ease-in-out"
184180 style = { { minWidth : '3px' , height : '100%' } }
0 commit comments