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>
This commit is contained in:
167
src/components/project/phase-navigator.tsx
Normal file
167
src/components/project/phase-navigator.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user