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:
122
src/components/project/health-widget.tsx
Normal file
122
src/components/project/health-widget.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user