Files
mylder-frontend/src/components/project/phase-navigator.tsx
christiankrag 884bbb11fc Add design thinking components for project management
- Add PhaseNavigator with 5 design phases (Empathize, Define, Ideate, Prototype, Test)
- Add HealthWidget with circular progress and metrics
- Add BacklogBoard with WSJF prioritization display
- Add RecommendationsWidget for AI-powered insights
- Add Tooltip component (shadcn/ui)
- Add design-thinking types with phase configs and backlog item structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 19:54:05 +01:00

168 lines
7.0 KiB
TypeScript

'use client'
import { motion } from 'motion/react'
import { cn } from '@/lib/utils'
import { Check, Loader2, AlertCircle, RotateCcw } from 'lucide-react'
import { DESIGN_PHASES, designPhases, type DesignPhase, type PhaseStatus } from '@/types/design-thinking'
import { Button } from '@/components/ui/button'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
interface PhaseNavigatorProps {
currentPhase: DesignPhase
phaseStatuses: Record<DesignPhase, PhaseStatus>
onPhaseClick?: (phase: DesignPhase) => void
onLoopBack?: (fromPhase: DesignPhase, toPhase: DesignPhase) => void
className?: string
variant?: 'horizontal' | 'compact'
}
const colorMap: Record<string, { bg: string; border: string; text: string; ring: string }> = {
purple: { bg: 'bg-purple-500/10', border: 'border-purple-500/50', text: 'text-purple-500', ring: 'ring-purple-500/30' },
blue: { bg: 'bg-blue-500/10', border: 'border-blue-500/50', text: 'text-blue-500', ring: 'ring-blue-500/30' },
amber: { bg: 'bg-amber-500/10', border: 'border-amber-500/50', text: 'text-amber-500', ring: 'ring-amber-500/30' },
orange: { bg: 'bg-orange-500/10', border: 'border-orange-500/50', text: 'text-orange-500', ring: 'ring-orange-500/30' },
emerald: { bg: 'bg-emerald-500/10', border: 'border-emerald-500/50', text: 'text-emerald-500', ring: 'ring-emerald-500/30' },
}
export function PhaseNavigator({
currentPhase,
phaseStatuses,
onPhaseClick,
onLoopBack,
className,
variant = 'horizontal'
}: PhaseNavigatorProps) {
const currentIndex = DESIGN_PHASES.indexOf(currentPhase)
const progress = ((currentIndex + 1) / DESIGN_PHASES.length) * 100
const getStatusIcon = (status: PhaseStatus) => {
switch (status) {
case 'completed':
return <Check className="w-4 h-4" />
case 'in_progress':
return <Loader2 className="w-4 h-4 animate-spin" />
case 'blocked':
case 'needs_review':
return <AlertCircle className="w-4 h-4" />
default:
return null
}
}
return (
<TooltipProvider>
<div className={cn('w-full', className)}>
{/* Progress bar */}
<div className="relative h-1.5 bg-muted rounded-full overflow-hidden mb-6">
<motion.div
className="absolute inset-y-0 left-0 bg-gradient-to-r from-purple-500 via-blue-500 via-amber-500 via-orange-500 to-emerald-500"
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5, ease: 'easeOut' }}
/>
</div>
{/* Phase nodes */}
<div className="flex justify-between items-start">
{designPhases.map((phase, index) => {
const status = phaseStatuses[phase.id]
const isCurrent = phase.id === currentPhase
const isCompleted = status === 'completed'
const isBlocked = status === 'blocked' || status === 'needs_review'
const colors = colorMap[phase.color]
return (
<div key={phase.id} className="flex flex-col items-center flex-1">
{/* Connector line */}
{index > 0 && (
<div className="absolute" style={{ left: `${((index - 0.5) / DESIGN_PHASES.length) * 100}%`, top: '0.75rem', width: `${(1 / DESIGN_PHASES.length) * 100}%` }}>
<div className={cn(
'h-0.5 w-full transition-colors duration-300',
index <= currentIndex ? 'bg-foreground/20' : 'bg-muted'
)} />
</div>
)}
{/* Phase node */}
<Tooltip>
<TooltipTrigger asChild>
<motion.button
onClick={() => onPhaseClick?.(phase.id)}
className={cn(
'relative w-12 h-12 rounded-full flex items-center justify-center text-xl transition-all duration-300 border-2',
isCurrent && `${colors.bg} ${colors.border} ring-4 ${colors.ring}`,
isCompleted && 'bg-emerald-500/10 border-emerald-500/50',
isBlocked && 'bg-red-500/10 border-red-500/50',
!isCurrent && !isCompleted && !isBlocked && 'bg-muted border-transparent'
)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
animate={isCurrent ? { scale: [1, 1.05, 1] } : {}}
transition={{ repeat: isCurrent ? Infinity : 0, duration: 2 }}
>
<span>{phase.icon}</span>
{/* Status indicator */}
{(isCompleted || status === 'in_progress' || isBlocked) && (
<div className={cn(
'absolute -top-1 -right-1 w-5 h-5 rounded-full flex items-center justify-center',
isCompleted && 'bg-emerald-500 text-white',
status === 'in_progress' && 'bg-blue-500 text-white',
isBlocked && 'bg-red-500 text-white'
)}>
{getStatusIcon(status)}
</div>
)}
</motion.button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
<div className="space-y-1">
<p className="font-semibold">{phase.label}</p>
<p className="text-xs text-muted-foreground">{phase.fullDescription}</p>
<div className="flex flex-wrap gap-1 mt-2">
{phase.commands.map(cmd => (
<code key={cmd} className="text-xs bg-muted px-1 rounded">{cmd}</code>
))}
</div>
</div>
</TooltipContent>
</Tooltip>
{/* Labels */}
<span className={cn(
'text-sm font-medium mt-2 transition-colors',
isCurrent ? colors.text : 'text-muted-foreground'
)}>
{phase.label}
</span>
<span className="text-xs text-muted-foreground hidden sm:block">
{phase.shortDescription}
</span>
</div>
)
})}
</div>
{/* Loop back indicator */}
{onLoopBack && currentIndex > 0 && (
<div className="flex justify-center mt-6">
<Button
variant="ghost"
size="sm"
className="gap-2 text-muted-foreground hover:text-foreground"
onClick={() => onLoopBack(currentPhase, DESIGN_PHASES[currentIndex - 1])}
>
<RotateCcw className="w-4 h-4" />
Loop back to {designPhases[currentIndex - 1].label}
</Button>
</div>
)}
</div>
</TooltipProvider>
)
}