- 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>
123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { motion } from 'motion/react'
|
|
import { cn } from '@/lib/utils'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Activity, AlertTriangle, CheckCircle2, Clock, TrendingUp } from 'lucide-react'
|
|
import type { ProjectHealth } from '@/types/design-thinking'
|
|
|
|
interface HealthWidgetProps {
|
|
health: ProjectHealth
|
|
className?: string
|
|
}
|
|
|
|
function HealthRing({ value, size = 120, strokeWidth = 8 }: { value: number; size?: number; strokeWidth?: number }) {
|
|
const radius = (size - strokeWidth) / 2
|
|
const circumference = radius * 2 * Math.PI
|
|
const offset = circumference - (value / 100) * circumference
|
|
|
|
const getColor = (v: number) => {
|
|
if (v >= 80) return 'stroke-emerald-500'
|
|
if (v >= 60) return 'stroke-amber-500'
|
|
return 'stroke-red-500'
|
|
}
|
|
|
|
return (
|
|
<div className="relative" style={{ width: size, height: size }}>
|
|
<svg className="transform -rotate-90" width={size} height={size}>
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
strokeWidth={strokeWidth}
|
|
className="stroke-muted fill-none"
|
|
/>
|
|
<motion.circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
strokeWidth={strokeWidth}
|
|
className={cn('fill-none', getColor(value))}
|
|
strokeLinecap="round"
|
|
initial={{ strokeDashoffset: circumference }}
|
|
animate={{ strokeDashoffset: offset }}
|
|
transition={{ duration: 1, ease: 'easeOut' }}
|
|
style={{ strokeDasharray: circumference }}
|
|
/>
|
|
</svg>
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
<motion.span
|
|
className="text-3xl font-bold"
|
|
initial={{ opacity: 0, scale: 0.5 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ delay: 0.5 }}
|
|
>
|
|
{value}
|
|
</motion.span>
|
|
<span className="text-xs text-muted-foreground">Health Score</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function HealthWidget({ health, className }: HealthWidgetProps) {
|
|
const metrics = [
|
|
{
|
|
label: 'Velocity',
|
|
value: health.velocity,
|
|
icon: TrendingUp,
|
|
color: health.velocity >= 80 ? 'text-emerald-500' : health.velocity >= 60 ? 'text-amber-500' : 'text-red-500'
|
|
},
|
|
{
|
|
label: 'Completion',
|
|
value: health.completion_rate,
|
|
icon: CheckCircle2,
|
|
color: health.completion_rate >= 80 ? 'text-emerald-500' : health.completion_rate >= 60 ? 'text-amber-500' : 'text-red-500',
|
|
suffix: '%'
|
|
},
|
|
{
|
|
label: 'Blockers',
|
|
value: health.blockers,
|
|
icon: AlertTriangle,
|
|
color: health.blockers === 0 ? 'text-emerald-500' : health.blockers <= 2 ? 'text-amber-500' : 'text-red-500',
|
|
inverse: true
|
|
},
|
|
{
|
|
label: 'Overdue',
|
|
value: health.overdue,
|
|
icon: Clock,
|
|
color: health.overdue === 0 ? 'text-emerald-500' : health.overdue <= 2 ? 'text-amber-500' : 'text-red-500',
|
|
inverse: true
|
|
}
|
|
]
|
|
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader className="pb-2">
|
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
|
<Activity className="w-4 h-4" />
|
|
Project Health
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex items-center gap-6">
|
|
<HealthRing value={health.overall} />
|
|
<div className="grid grid-cols-2 gap-4 flex-1">
|
|
{metrics.map((metric) => (
|
|
<div key={metric.label} className="space-y-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<metric.icon className={cn('w-3.5 h-3.5', metric.color)} />
|
|
<span className="text-xs text-muted-foreground">{metric.label}</span>
|
|
</div>
|
|
<p className={cn('text-lg font-semibold', metric.color)}>
|
|
{metric.value}{metric.suffix || ''}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|