- 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>
168 lines
7.0 KiB
TypeScript
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>
|
|
)
|
|
}
|