1- import type { CSSProperties } from 'react' ;
1+ import React from 'react' ;
22
33export interface Step {
44 index : number ;
@@ -14,65 +14,66 @@ interface ProgressProps {
1414
1515type StepState = 'completed' | 'current' | 'upcoming' ;
1616
17- const COLORS = {
17+ const DOT_SIZE = 32 ;
18+
19+ const COLOR = {
1820 completed : '#22c55e' ,
1921 current : '#38bdf8' ,
20- upcoming : 'rgba(255,255,255,0.2) ' ,
21- line : 'rgba(255,255,255,0.15) ' ,
22- lineFilled : '#22c55e ' ,
23- text : 'rgba(255,255,255,0.9)' ,
24- textDim : 'rgba(255,255,255,0.4 )' ,
22+ upcoming : 'transparent ' ,
23+ lineActive : '#22c55e ' ,
24+ lineInactive : 'rgba(255,255,255,0.15) ' ,
25+ textBright : 'rgba(255,255,255,0.9)' ,
26+ textDim : 'rgba(255,255,255,0.55 )' ,
2527} ;
2628
27- function stepState ( index : number , current : number , completed : ReadonlySet < number > ) : StepState {
29+ function getState (
30+ index : number ,
31+ current : number ,
32+ completed : ReadonlySet < number > ,
33+ ) : StepState {
2834 if ( completed . has ( index ) ) return 'completed' ;
2935 if ( index === current ) return 'current' ;
3036 return 'upcoming' ;
3137}
3238
3339function Dot ( { state, number} : { state : StepState ; number : number } ) {
34- const base : CSSProperties = {
35- width : 32 ,
36- height : 32 ,
40+ const shared = {
41+ width : DOT_SIZE ,
42+ height : DOT_SIZE ,
3743 borderRadius : '50%' ,
3844 display : 'flex' ,
3945 alignItems : 'center' ,
4046 justifyContent : 'center' ,
4147 fontFamily : 'sans-serif' ,
4248 fontWeight : 700 ,
4349 fontSize : 13 ,
44- flexShrink : 0 ,
4550 transition : 'background 0.25s, box-shadow 0.25s' ,
46- } ;
51+ flexShrink : 0 ,
52+ } as const ;
4753
4854 if ( state === 'completed' ) {
49- return (
50- < div style = { { ...base , background : COLORS . completed , color : '#fff' } } >
51- ✓
52- </ div >
53- ) ;
55+ return < div style = { { ...shared , background : COLOR . completed , color : '#fff' } } > ✓</ div > ;
5456 }
5557 if ( state === 'current' ) {
5658 return (
5759 < div
5860 style = { {
59- ...base ,
60- background : COLORS . current ,
61+ ...shared ,
62+ background : COLOR . current ,
6163 color : '#fff' ,
62- boxShadow : ` 0 0 0 4px rgba(56,189,248,0.3)` ,
64+ boxShadow : ' 0 0 0 4px rgba(56,189,248,0.3)' ,
6365 } }
6466 >
6567 { number }
6668 </ div >
6769 ) ;
6870 }
69- // Upcoming: white outline — looks like an unselected tab, not a locked step.
7071 return (
7172 < div
7273 style = { {
73- ...base ,
74- background : 'transparent' ,
75- border : ` 2px solid rgba(255,255,255,0.5)` ,
74+ ...shared ,
75+ background : COLOR . upcoming ,
76+ border : ' 2px solid rgba(255,255,255,0.5)' ,
7677 color : 'rgba(255,255,255,0.7)' ,
7778 } }
7879 >
@@ -81,7 +82,12 @@ function Dot({state, number}: {state: StepState; number: number}) {
8182 ) ;
8283}
8384
84- export default function Progress ( { steps, currentIndex, completedIndices, onNavigate} : ProgressProps ) {
85+ export default function Progress ( {
86+ steps,
87+ currentIndex,
88+ completedIndices,
89+ onNavigate,
90+ } : ProgressProps ) {
8591 return (
8692 < div
8793 style = { {
@@ -92,83 +98,99 @@ export default function Progress({steps, currentIndex, completedIndices, onNavig
9298 zIndex : 50 ,
9399 background : 'rgba(2,0,28,0.85)' ,
94100 backdropFilter : 'blur(8px)' ,
95- padding : '10px 24px 14px ' ,
101+ padding : '10px 24px 12px ' ,
96102 display : 'flex' ,
97103 alignItems : 'center' ,
98104 justifyContent : 'center' ,
99105 } }
100106 >
101- < div
102- style = { {
103- display : 'flex' ,
104- alignItems : 'flex-start' ,
105- gap : 0 ,
106- maxWidth : 600 ,
107- width : '100%' ,
108- } }
109- >
110- { steps . map ( ( step , i ) => {
111- const state = stepState ( step . index , currentIndex , completedIndices ) ;
112- const isLast = i === steps . length - 1 ;
113-
114- return (
115- < div
116- key = { step . index }
117- style = { { display : 'flex' , alignItems : 'flex-start' , flex : isLast ? 0 : 1 } }
118- >
119- { /* Step */ }
120- < button
121- data-testid = { `step-${ step . index } ` }
122- data-state = { stepState ( step . index , currentIndex , completedIndices ) }
123- onClick = { ( ) => onNavigate ( step . index ) }
124- title = { step . label }
125- style = { {
126- background : 'none' ,
127- border : 'none' ,
128- cursor : 'pointer' ,
129- padding : 0 ,
130- display : 'flex' ,
131- flexDirection : 'column' ,
132- alignItems : 'center' ,
133- gap : 4 ,
134- flexShrink : 0 ,
135- } }
136- >
137- < Dot state = { state } number = { step . index + 1 } />
138- < span
107+ < div style = { { width : '100%' , maxWidth : 640 } } >
108+ { /* Row 1: dots + connector lines — fixed height, connectors stay centered */ }
109+ < div style = { { display : 'flex' , alignItems : 'center' } } >
110+ { steps . map ( ( step , i ) => {
111+ const state = getState ( step . index , currentIndex , completedIndices ) ;
112+ const isLast = i === steps . length - 1 ;
113+ return (
114+ < React . Fragment key = { step . index } >
115+ < button
116+ data-testid = { `step-${ step . index } ` }
117+ data-state = { state }
118+ onClick = { ( ) => onNavigate ( step . index ) }
139119 style = { {
140- fontSize : 10 ,
141- fontFamily : 'sans-serif' ,
142- color : state === 'upcoming' ? 'rgba(255,255,255,0.6)' : COLORS . text ,
143- whiteSpace : 'nowrap' ,
144- maxWidth : 72 ,
145- textAlign : 'center' ,
146- lineHeight : 1.2 ,
147- transition : 'color 0.25s' ,
120+ background : 'none' ,
121+ border : 'none' ,
122+ cursor : 'pointer' ,
123+ padding : 0 ,
124+ flexShrink : 0 ,
125+ lineHeight : 0 ,
148126 } }
127+ aria-label = { step . label }
149128 >
150- { step . label }
151- </ span >
152- </ button >
129+ < Dot state = { state } number = { step . index + 1 } />
130+ </ button >
131+ { ! isLast && (
132+ < div
133+ style = { {
134+ flex : 1 ,
135+ height : 2 ,
136+ background : completedIndices . has ( step . index )
137+ ? COLOR . lineActive
138+ : COLOR . lineInactive ,
139+ transition : 'background 0.4s' ,
140+ minWidth : 12 ,
141+ } }
142+ />
143+ ) }
144+ </ React . Fragment >
145+ ) ;
146+ } ) }
147+ </ div >
153148
154- { /* Connector line */ }
155- { ! isLast && (
149+ { /* Row 2: labels centered under each dot */ }
150+ < div
151+ style = { {
152+ display : 'flex' ,
153+ alignItems : 'flex-start' ,
154+ marginTop : 6 ,
155+ } }
156+ >
157+ { steps . map ( ( step , i ) => {
158+ const state = getState ( step . index , currentIndex , completedIndices ) ;
159+ const isLast = i === steps . length - 1 ;
160+ return (
161+ < React . Fragment key = { step . index } >
162+ { /* Label occupies the same width as the dot so it centers under it */ }
156163 < div
157164 style = { {
158- flex : 1 ,
159- height : 2 ,
160- marginTop : 15 ,
161- background : completedIndices . has ( step . index )
162- ? COLORS . lineFilled
163- : COLORS . line ,
164- transition : 'background 0.4s' ,
165- minWidth : 16 ,
165+ width : DOT_SIZE ,
166+ flexShrink : 0 ,
167+ display : 'flex' ,
168+ justifyContent : 'center' ,
166169 } }
167- />
168- ) }
169- </ div >
170- ) ;
171- } ) }
170+ >
171+ < span
172+ style = { {
173+ display : 'block' ,
174+ width : 72 ,
175+ textAlign : 'center' ,
176+ fontSize : 10 ,
177+ fontFamily : 'sans-serif' ,
178+ lineHeight : 1.3 ,
179+ color : state === 'upcoming' ? COLOR . textDim : COLOR . textBright ,
180+ transition : 'color 0.25s' ,
181+ wordBreak : 'break-word' ,
182+ hyphens : 'auto' ,
183+ } }
184+ >
185+ { step . label }
186+ </ span >
187+ </ div >
188+ { /* Spacer that mirrors the connector line so labels stay aligned */ }
189+ { ! isLast && < div style = { { flex : 1 , minWidth : 12 } } /> }
190+ </ React . Fragment >
191+ ) ;
192+ } ) }
193+ </ div >
172194 </ div >
173195 </ div >
174196 ) ;
0 commit comments