@@ -13,100 +13,151 @@ interface StepRunCardProps {
1313 subSteps : StepRunCardSubStep [ ]
1414 description ?: string
1515 isRunning : boolean
16+ completed ?: boolean
17+ showRunButton ?: boolean
1618 onRun : ( ) => void
1719 disabled : boolean
1820}
1921
20- export function StepRunCard ( { stepSlug, subSteps, description, isRunning, onRun, disabled } : StepRunCardProps ) {
22+ const HOVER_BG_BY_COLOR : Record < string , string > = {
23+ "bg-gray-500" : "hover:bg-gray-500" ,
24+ "bg-blue-500" : "hover:bg-blue-500" ,
25+ "bg-violet-500" : "hover:bg-violet-500" ,
26+ "bg-orange-500" : "hover:bg-orange-500" ,
27+ "bg-teal-500" : "hover:bg-teal-500" ,
28+ "bg-lime-500" : "hover:bg-lime-500" ,
29+ "bg-pink-500" : "hover:bg-pink-500" ,
30+ "bg-amber-500" : "hover:bg-amber-500" ,
31+ }
32+
33+ export function StepRunCard ( {
34+ stepSlug,
35+ subSteps,
36+ description,
37+ isRunning,
38+ completed,
39+ showRunButton = true ,
40+ onRun,
41+ disabled,
42+ } : StepRunCardProps ) {
2143 const stepConfig = STEPS . find ( ( s ) => s . slug === stepSlug )
2244 const { progress } = useStepRun ( )
2345 const { subSteps : subStepProgress , error, targetSteps } = progress
2446
2547 const Icon = stepConfig ?. icon ?? Play
2648 const bgDark = stepConfig ?. bgDark ?? "bg-gray-700"
2749 const color = stepConfig ?. color ?? "bg-gray-500"
50+ const borderColor = stepConfig ?. borderColor ?? "border-gray-200"
2851 const hasError = ! ! error && targetSteps . has ( stepSlug )
52+ const isCompleted = completed || progress . steps . get ( stepSlug ) ?. state === "done"
53+ const hasSubSteps = subSteps . length > 0
54+ const hoverColorClass = HOVER_BG_BY_COLOR [ color ] ?? "hover:bg-gray-500"
55+ const buttonToneClass = isCompleted
56+ ? cn ( color , "text-white" )
57+ : cn ( "bg-gray-200 text-gray-700" , hoverColorClass , "hover:text-white" )
2958
3059 return (
31- < div className = "rounded-lg border bg-card overflow-hidden max-w-xl" >
60+ < div className = { cn ( "rounded-lg border bg-card overflow-hidden max-w-xl" , borderColor ) } >
3261 { /* Colored header */ }
33- < div className = { cn ( "px-4 py-3 flex items-center gap-2.5 text-white" , bgDark ) } >
62+ < div className = { cn ( "px-4 py-2 flex items-center gap-2.5 text-white" , bgDark ) } >
3463 < div className = "flex items-center justify-center w-6 h-6 rounded-full bg-white/20" >
3564 < Icon className = "w-3 h-3" />
3665 </ div >
3766 < span className = "text-sm font-semibold" >
3867 { isRunning
39- ? `Running ${ stepConfig ?. label ?. toLowerCase ( ) ?? stepSlug } ...`
68+ ? `${ stepConfig ?. runningLabel ?? stepSlug } ...`
4069 : stepConfig ?. label ?? stepSlug }
4170 </ span >
4271 </ div >
4372
4473 { /* Main row: sub-steps | button | description */ }
45- < div className = "flex items-center gap-5 p-5" >
74+ < div
75+ className = { cn (
76+ "flex items-center px-5 py-3" ,
77+ showRunButton || hasSubSteps ? "gap-5" : "justify-center"
78+ ) }
79+ >
4680 { /* Sub-steps */ }
47- < div className = "space-y-2.5 shrink-0" >
48- { subSteps . map ( ( { key, label } ) => {
49- const sub = subStepProgress . get ( key )
50- const isDone = sub ?. state === "done"
51- const isSubRunning = sub ?. state === "running"
52- const isError = sub ?. state === "error"
53- const hasPages = sub ?. page != null && sub ?. totalPages != null && sub . totalPages > 0
81+ { hasSubSteps && (
82+ < div className = "space-y-1.5 w-48 shrink-0" >
83+ { subSteps . map ( ( { key, label } ) => {
84+ const sub = subStepProgress . get ( key )
85+ const isDone = sub ?. state === "done" || ( completed && ! sub )
86+ const isSubRunning = sub ?. state === "running"
87+ const isError = sub ?. state === "error"
88+ const hasPages = sub ?. page != null && sub ?. totalPages != null && sub . totalPages > 0
5489
55- return (
56- < div
57- key = { key }
58- className = { cn (
59- "flex items-center gap-2.5 text-xs whitespace-nowrap" ,
60- isDone ? "text-muted-foreground" : isError ? "text-red-500" : isSubRunning ? "text-foreground" : "text-muted-foreground/50" ,
61- ) }
62- >
63- { isDone ? (
64- < Check className = "w-4 h-4 text-green-500 shrink-0" />
65- ) : isError ? (
66- < XCircle className = "w-4 h-4 text-red-500 shrink-0" />
67- ) : isSubRunning ? (
68- < Loader2 className = "w-4 h-4 animate-spin text-blue-500 shrink-0" />
69- ) : (
70- < div className = "w-4 h-4 rounded-full border border-current opacity-30 shrink-0" />
71- ) }
72- < span > { label } </ span >
73- { isSubRunning && hasPages && (
74- < span className = "text-muted-foreground tabular-nums" > { sub . page } /{ sub . totalPages } </ span >
75- ) }
76- </ div >
77- )
78- } ) }
79- </ div >
90+ return (
91+ < div
92+ key = { key }
93+ className = { cn (
94+ "flex items-center gap-2.5 text-xs whitespace-nowrap" ,
95+ isDone ? "text-muted-foreground" : isError ? "text-red-500" : isSubRunning ? "text-foreground" : "text-muted-foreground/50" ,
96+ ) }
97+ >
98+ { isDone ? (
99+ < Check className = "w-4 h-4 text-green-500 shrink-0" />
100+ ) : isError ? (
101+ < XCircle className = "w-4 h-4 text-red-500 shrink-0" />
102+ ) : isSubRunning ? (
103+ < Loader2 className = "w-4 h-4 animate-spin text-blue-500 shrink-0" />
104+ ) : (
105+ < div className = "w-4 h-4 rounded-full border border-current opacity-30 shrink-0" />
106+ ) }
107+ < span > { label } </ span >
108+ { isSubRunning && hasPages && (
109+ < span className = "text-muted-foreground tabular-nums" > { sub . page } /{ sub . totalPages } </ span >
110+ ) }
111+ </ div >
112+ )
113+ } ) }
114+ </ div >
115+ ) }
80116
81117 { /* Play / Retry / Spinner button */ }
82- < div className = "shrink-0" >
83- { isRunning ? (
84- < div className = { cn (
85- "flex items-center justify-center w-12 h-12 rounded-full opacity-60" ,
86- color , "text-white" ,
87- ) } >
88- < Loader2 className = "w-5 h-5 animate-spin" />
89- </ div >
90- ) : (
91- < button
92- type = "button"
93- className = { cn (
94- "flex items-center justify-center w-12 h-12 rounded-full transition-all cursor-pointer" ,
95- "hover:scale-105 active:scale-95 disabled:opacity-30 disabled:cursor-default disabled:hover:scale-100" ,
118+ { showRunButton && (
119+ < div className = "shrink-0" >
120+ { isRunning ? (
121+ < div className = { cn (
122+ "flex items-center justify-center w-12 h-12 rounded-full opacity-60" ,
96123 color , "text-white" ,
97- ) }
98- disabled = { disabled }
99- onClick = { onRun }
100- title = { hasError ? "Retry" : `Run ${ stepConfig ?. label ?. toLowerCase ( ) ?? stepSlug } ` }
101- >
102- { hasError ? < RotateCcw className = "w-5 h-5" /> : < Play className = "w-5 h-5 ml-0.5" /> }
103- </ button >
104- ) }
105- </ div >
124+ ) } >
125+ < Loader2 className = "w-5 h-5 animate-spin" />
126+ </ div >
127+ ) : (
128+ < button
129+ type = "button"
130+ className = { cn (
131+ "flex items-center justify-center w-12 h-12 rounded-full transition-all cursor-pointer" ,
132+ "hover:scale-105 active:scale-95 disabled:opacity-30 disabled:cursor-default disabled:hover:scale-100" ,
133+ buttonToneClass ,
134+ ) }
135+ disabled = { disabled }
136+ onClick = { ( e ) => { e . stopPropagation ( ) ; e . preventDefault ( ) ; onRun ( ) } }
137+ title = {
138+ hasError
139+ ? "Retry"
140+ : isCompleted
141+ ? `Re-run ${ stepConfig ?. label ?. toLowerCase ( ) ?? stepSlug } `
142+ : `Run ${ stepConfig ?. label ?. toLowerCase ( ) ?? stepSlug } `
143+ }
144+ >
145+ { hasError || isCompleted ? < RotateCcw className = "w-5 h-5" /> : < Play className = "w-5 h-5 ml-0.5" /> }
146+ </ button >
147+ ) }
148+ </ div >
149+ ) }
106150
107151 { /* Description */ }
108152 { description && (
109- < p className = "flex-1 min-w-0 text-xs text-muted-foreground leading-relaxed" > { description } </ p >
153+ < p
154+ className = { cn (
155+ "min-w-0 text-xs text-muted-foreground leading-relaxed" ,
156+ showRunButton || hasSubSteps ? "flex-1" : "max-w-md text-center"
157+ ) }
158+ >
159+ { description }
160+ </ p >
110161 ) }
111162 </ div >
112163
0 commit comments