1- ' use client'
1+ " use client" ;
22
3- import React , { useMemo , useCallback } from ' react'
4- import { Card } from "@/components/ui/card"
5- import { ChevronRight } from ' lucide-react'
6- import {
7- DecisionWorkflowSteps ,
8- DecisionWorkflowStep ,
3+ import React from " react" ;
4+ import { Card } from "@/components/ui/card" ;
5+ import { ChevronRight } from " lucide-react" ;
6+ import {
7+ DecisionWorkflowSteps ,
8+ DecisionWorkflowStep ,
99 DecisionWorkflowStepKey ,
1010 DecisionWorkflowStepsSequence ,
1111 WorkflowNavigator ,
1212 StepRoles ,
13- } from '@/lib/domain/Decision'
14- import { cn } from "@/lib/utils"
15-
16- const WORKFLOW_CONFIG = {
17- STEP_SIZE : {
18- ICON : 'h-12 w-12' ,
19- ARROW : 'w-5 h-5' ,
20- } ,
21- COLORS : {
22- ACTIVE : 'bg-primary text-primary-foreground' ,
23- INACTIVE : 'bg-muted text-muted-foreground' ,
24- COMPLETED : 'bg-primary text-primary-foreground' ,
25- DISABLED : 'bg-muted text-muted-foreground cursor-not-allowed' ,
26- } ,
27- } as const ;
13+ } from "@/lib/domain/Decision" ;
14+ import { cn } from "@/lib/utils" ;
2815
2916interface HorizontalWorkflowProgressProps {
3017 currentStep ?: DecisionWorkflowStep ;
31- onStepClick ?: ( step : DecisionWorkflowStep ) => void ;
18+ onStepChange ?: ( step : DecisionWorkflowStep ) => void ;
3219 allowFutureSteps ?: boolean ;
3320 showRoles ?: boolean ;
3421 className ?: string ;
35- size ?: 'sm' | 'md' | 'lg' ;
3622}
3723
38- export default function HorizontalWorkflowProgress ( {
24+ export default function HorizontalWorkflowProgress ( {
3925 currentStep = DecisionWorkflowSteps . IDENTIFY ,
40- onStepClick ,
26+ onStepChange ,
4127 allowFutureSteps = false ,
4228 showRoles = true ,
4329 className,
44- size = 'md' ,
4530} : HorizontalWorkflowProgressProps ) {
46- const currentStepIndex = useMemo ( ( ) =>
47- WorkflowNavigator . getStepIndex ( currentStep ) ,
48- [ currentStep ]
49- ) ;
31+ const currentStepIndex = WorkflowNavigator . getStepIndex ( currentStep ) ;
5032
51- const getStepSize = useCallback ( ( size : 'sm' | 'md' | 'lg' ) => {
52- switch ( size ) {
53- case 'sm' :
54- return 'h-8 w-8' ;
55- case 'lg' :
56- return 'h-16 w-16' ;
57- default :
58- return WORKFLOW_CONFIG . STEP_SIZE . ICON ;
59- }
60- } , [ ] ) ;
33+ return (
34+ < Card className = { cn ( "p-6" , className ) } >
35+ < div
36+ className = "flex items-center justify-between"
37+ role = "progressbar"
38+ aria-valuemin = { 0 }
39+ aria-valuemax = { DecisionWorkflowStepsSequence . length }
40+ aria-valuenow = { currentStepIndex + 1 }
41+ >
42+ { DecisionWorkflowStepsSequence . map ( ( step , index ) => {
43+ const isActive = step . key === currentStep . key ;
44+ const isCompleted = index < currentStepIndex ;
45+ const isClickable =
46+ onStepChange && ( allowFutureSteps || isCompleted || isActive ) ;
47+ const role =
48+ StepRoles [ step . key . toUpperCase ( ) as DecisionWorkflowStepKey ] ;
6149
62- const renderStep = useCallback ( ( step : DecisionWorkflowStep , index : number ) => {
63- const isActive = step . key === currentStep . key ;
64- const isCompleted = index < currentStepIndex ;
65- const isClickable = onStepClick && ( allowFutureSteps || isCompleted || isActive ) ;
66- const role = StepRoles [ step . key . toUpperCase ( ) as DecisionWorkflowStepKey ] ;
67- const stepSize = getStepSize ( size ) ;
50+ return (
51+ < React . Fragment key = { step . key } >
52+ { /* Step with icon and label */ }
53+ < div className = "flex flex-col items-center gap-2" >
54+ { showRoles && (
55+ < div
56+ className = { cn (
57+ "inline-flex justify-center rounded-full px-3 py-1 text-xs font-semibold" ,
58+ isCompleted || isActive
59+ ? "bg-primary/10 text-primary"
60+ : "bg-muted text-muted-foreground" ,
61+ ) }
62+ >
63+ { role }
64+ </ div >
65+ ) }
6866
69- return (
70- < React . Fragment key = { step . key } >
71- { /* Step circle with icon and role */ }
72- < div className = "flex flex-col items-center gap-2" >
73- { showRoles && (
74- < div
75- className = { cn (
76- "inline-flex justify-center rounded-full px-3 py-1 text-xs font-semibold tracking-wide" ,
77- isCompleted || isActive
78- ? 'bg-primary/10 text-primary'
79- : 'bg-muted text-muted-foreground'
80- ) }
81- >
82- { role }
83- </ div >
84- ) }
85- < button
86- onClick = { ( ) => isClickable && onStepClick ?.( step ) }
87- className = { cn (
88- "relative flex items-center justify-center rounded-full border-2 transition-colors" ,
89- stepSize ,
90- isClickable && "cursor-pointer hover:bg-primary/90" ,
91- isCompleted || isActive
92- ? WORKFLOW_CONFIG . COLORS . COMPLETED
93- : WORKFLOW_CONFIG . COLORS . INACTIVE ,
94- ! isClickable && WORKFLOW_CONFIG . COLORS . DISABLED
95- ) }
96- disabled = { ! isClickable }
97- aria-current = { isActive ? 'step' : undefined }
98- aria-label = { `${ step . label } step, ${ isCompleted ? 'completed' : isActive ? 'current' : 'upcoming' } ` }
99- >
100- < step . icon className = "w-5 h-5" aria-hidden = "true" />
101- </ button >
102- < span
103- className = { cn (
104- "text-sm font-medium" ,
105- isCompleted || isActive ? "text-foreground" : "text-muted-foreground"
106- ) }
107- >
108- { step . label }
109- </ span >
110- </ div >
111-
112- { /* Connector line with arrow */ }
113- { ! WorkflowNavigator . isLastStep ( step ) && (
114- < div className = "flex-1 flex items-center" >
115- < div
116- className = { cn (
117- "h-0.5 flex-1" ,
118- isCompleted ? "bg-primary" : "bg-muted"
119- ) }
120- role = "presentation"
121- />
122- < ChevronRight
123- className = { cn (
124- WORKFLOW_CONFIG . STEP_SIZE . ARROW ,
125- "shrink-0" ,
126- isCompleted ? "text-primary" : "text-muted"
127- ) }
128- aria-hidden = "true"
129- />
130- </ div >
131- ) }
132- </ React . Fragment >
133- ) ;
134- } , [ currentStep , currentStepIndex , onStepClick , allowFutureSteps , showRoles , size , getStepSize ] ) ;
67+ < button
68+ onClick = { ( ) => isClickable && onStepChange ?.( step ) }
69+ className = { cn (
70+ "flex h-12 w-12 items-center justify-center rounded-full border-2 transition-colors" ,
71+ isClickable && "cursor-pointer hover:bg-primary/90" ,
72+ isCompleted || isActive
73+ ? "bg-primary text-primary-foreground"
74+ : "bg-muted text-muted-foreground" ,
75+ ! isClickable && "cursor-not-allowed" ,
76+ ) }
77+ disabled = { ! isClickable }
78+ aria-current = { isActive ? "step" : undefined }
79+ >
80+ < step . icon className = "h-5 w-5" aria-hidden = "true" />
81+ </ button >
13582
136- return (
137- < Card className = { cn ( "p-8" , className ) } >
138- < div className = "flex items-center justify-between" role = "progressbar" aria-valuemin = { 0 } aria-valuemax = { DecisionWorkflowStepsSequence . length } aria-valuenow = { currentStepIndex + 1 } >
139- { DecisionWorkflowStepsSequence . map ( renderStep ) }
83+ < span
84+ className = { cn (
85+ "text-sm font-medium" ,
86+ isCompleted || isActive
87+ ? "text-foreground"
88+ : "text-muted-foreground" ,
89+ ) }
90+ >
91+ { step . label }
92+ </ span >
93+ </ div >
94+
95+ { /* Connector line */ }
96+ { index < DecisionWorkflowStepsSequence . length - 1 && (
97+ < div className = "flex-1 flex items-center" >
98+ < div
99+ className = { cn (
100+ "h-0.5 flex-1" ,
101+ isCompleted ? "bg-primary" : "bg-muted" ,
102+ ) }
103+ />
104+ < ChevronRight
105+ className = { cn (
106+ "h-5 w-5 shrink-0" ,
107+ isCompleted ? "text-primary" : "text-muted" ,
108+ ) }
109+ aria-hidden = "true"
110+ />
111+ </ div >
112+ ) }
113+ </ React . Fragment >
114+ ) ;
115+ } ) }
140116 </ div >
141117 </ Card >
142- )
143- }
118+ ) ;
119+ }
0 commit comments