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:
155
package-lock.json
generated
155
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@supabase/ssr": "^0.8.0",
|
"@supabase/ssr": "^0.8.0",
|
||||||
"@supabase/supabase-js": "^2.87.1",
|
"@supabase/supabase-js": "^2.87.1",
|
||||||
@@ -2471,6 +2472,96 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.8",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"@radix-ui/react-visually-hidden": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
@@ -2625,6 +2716,70 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/rect": {
|
"node_modules/@radix-ui/rect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@stripe/stripe-js": "^8.5.3",
|
"@stripe/stripe-js": "^8.5.3",
|
||||||
"@supabase/ssr": "^0.8.0",
|
"@supabase/ssr": "^0.8.0",
|
||||||
"@supabase/supabase-js": "^2.87.1",
|
"@supabase/supabase-js": "^2.87.1",
|
||||||
|
|||||||
249
src/components/project/backlog-board.tsx
Normal file
249
src/components/project/backlog-board.tsx
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { motion, AnimatePresence } from 'motion/react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
ArrowUp,
|
||||||
|
ArrowDown,
|
||||||
|
Clock,
|
||||||
|
Target,
|
||||||
|
AlertTriangle,
|
||||||
|
Zap,
|
||||||
|
MoreVertical,
|
||||||
|
GripVertical
|
||||||
|
} from 'lucide-react'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
import { designPhases, type BacklogItem, type DesignPhase } from '@/types/design-thinking'
|
||||||
|
|
||||||
|
interface BacklogBoardProps {
|
||||||
|
items: BacklogItem[]
|
||||||
|
onItemClick?: (item: BacklogItem) => void
|
||||||
|
onStatusChange?: (itemId: string, newStatus: BacklogItem['status']) => void
|
||||||
|
onPriorityChange?: (itemId: string, direction: 'up' | 'down') => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusColors: Record<BacklogItem['status'], string> = {
|
||||||
|
backlog: 'bg-muted text-muted-foreground',
|
||||||
|
ready: 'bg-blue-500/10 text-blue-500 border-blue-500/20',
|
||||||
|
in_progress: 'bg-amber-500/10 text-amber-500 border-amber-500/20',
|
||||||
|
done: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20',
|
||||||
|
blocked: 'bg-red-500/10 text-red-500 border-red-500/20'
|
||||||
|
}
|
||||||
|
|
||||||
|
function PriorityScore({ score }: { score: number }) {
|
||||||
|
const getColor = (s: number) => {
|
||||||
|
if (s >= 7) return 'text-emerald-500'
|
||||||
|
if (s >= 4) return 'text-amber-500'
|
||||||
|
return 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn('flex items-center gap-1', getColor(score))}>
|
||||||
|
<Zap className="w-3 h-3" />
|
||||||
|
<span className="text-xs font-medium">{score.toFixed(1)}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BacklogCard({
|
||||||
|
item,
|
||||||
|
onItemClick,
|
||||||
|
onStatusChange,
|
||||||
|
onPriorityChange
|
||||||
|
}: {
|
||||||
|
item: BacklogItem
|
||||||
|
onItemClick?: (item: BacklogItem) => void
|
||||||
|
onStatusChange?: (itemId: string, newStatus: BacklogItem['status']) => void
|
||||||
|
onPriorityChange?: (itemId: string, direction: 'up' | 'down') => void
|
||||||
|
}) {
|
||||||
|
const phase = designPhases.find(p => p.id === item.phase)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={cn(
|
||||||
|
'cursor-pointer transition-all hover:shadow-md',
|
||||||
|
item.status === 'blocked' && 'border-red-500/30'
|
||||||
|
)}
|
||||||
|
onClick={() => onItemClick?.(item)}
|
||||||
|
>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<GripVertical className="w-4 h-4 text-muted-foreground/50 mt-0.5 opacity-0 group-hover:opacity-100 transition-opacity cursor-grab" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-1">
|
||||||
|
<span className="text-sm font-medium truncate">{item.title}</span>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100">
|
||||||
|
<MoreVertical className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onStatusChange?.(item.id, 'ready') }}>
|
||||||
|
Mark Ready
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onStatusChange?.(item.id, 'in_progress') }}>
|
||||||
|
Start Work
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onStatusChange?.(item.id, 'done') }}>
|
||||||
|
Mark Done
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={(e) => { e.stopPropagation(); onStatusChange?.(item.id, 'blocked') }}>
|
||||||
|
Mark Blocked
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground line-clamp-2 mb-2">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className={statusColors[item.status]}>
|
||||||
|
{item.status.replace('_', ' ')}
|
||||||
|
</Badge>
|
||||||
|
{phase && (
|
||||||
|
<span className="text-xs">{phase.icon}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PriorityScore score={item.priority_score} />
|
||||||
|
<div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onPriorityChange?.(item.id, 'up') }}
|
||||||
|
>
|
||||||
|
<ArrowUp className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-5 w-5 p-0"
|
||||||
|
onClick={(e) => { e.stopPropagation(); onPriorityChange?.(item.id, 'down') }}
|
||||||
|
>
|
||||||
|
<ArrowDown className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* WSJF breakdown on hover */}
|
||||||
|
<div className="mt-2 pt-2 border-t border-dashed opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<div className="grid grid-cols-4 gap-1 text-xs">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Target className="w-3 h-3 text-blue-500" />
|
||||||
|
<span className="text-muted-foreground">{item.user_value}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3 text-amber-500" />
|
||||||
|
<span className="text-muted-foreground">{item.time_criticality}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-red-500" />
|
||||||
|
<span className="text-muted-foreground">{item.risk_reduction}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Zap className="w-3 h-3 text-purple-500" />
|
||||||
|
<span className="text-muted-foreground">E:{item.effort}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BacklogBoard({
|
||||||
|
items,
|
||||||
|
onItemClick,
|
||||||
|
onStatusChange,
|
||||||
|
onPriorityChange,
|
||||||
|
className
|
||||||
|
}: BacklogBoardProps) {
|
||||||
|
const [filter, setFilter] = useState<BacklogItem['status'] | 'all'>('all')
|
||||||
|
|
||||||
|
const filteredItems = filter === 'all'
|
||||||
|
? items
|
||||||
|
: items.filter(item => item.status === filter)
|
||||||
|
|
||||||
|
const sortedItems = [...filteredItems].sort((a, b) => b.priority_score - a.priority_score)
|
||||||
|
|
||||||
|
const statusCounts = {
|
||||||
|
backlog: items.filter(i => i.status === 'backlog').length,
|
||||||
|
ready: items.filter(i => i.status === 'ready').length,
|
||||||
|
in_progress: items.filter(i => i.status === 'in_progress').length,
|
||||||
|
done: items.filter(i => i.status === 'done').length,
|
||||||
|
blocked: items.filter(i => i.status === 'blocked').length,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-sm font-medium">Backlog</CardTitle>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{(['all', 'backlog', 'ready', 'in_progress', 'done', 'blocked'] as const).map((status) => (
|
||||||
|
<Button
|
||||||
|
key={status}
|
||||||
|
variant={filter === status ? 'secondary' : 'ghost'}
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
onClick={() => setFilter(status)}
|
||||||
|
>
|
||||||
|
{status === 'all' ? 'All' : status.replace('_', ' ')}
|
||||||
|
{status !== 'all' && (
|
||||||
|
<span className="ml-1 text-muted-foreground">
|
||||||
|
({statusCounts[status]})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 max-h-[500px] overflow-y-auto">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{sortedItems.map((item) => (
|
||||||
|
<BacklogCard
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
onStatusChange={onStatusChange}
|
||||||
|
onPriorityChange={onPriorityChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
{sortedItems.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-muted-foreground text-sm">
|
||||||
|
No items in {filter === 'all' ? 'backlog' : filter}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
src/components/project/index.ts
Normal file
4
src/components/project/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { PhaseNavigator } from './phase-navigator'
|
||||||
|
export { HealthWidget } from './health-widget'
|
||||||
|
export { BacklogBoard } from './backlog-board'
|
||||||
|
export { RecommendationsWidget } from './recommendations-widget'
|
||||||
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
155
src/components/project/recommendations-widget.tsx
Normal file
155
src/components/project/recommendations-widget.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { motion, AnimatePresence } from 'motion/react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
Lightbulb,
|
||||||
|
AlertTriangle,
|
||||||
|
TrendingUp,
|
||||||
|
Zap,
|
||||||
|
ChevronRight,
|
||||||
|
X
|
||||||
|
} from 'lucide-react'
|
||||||
|
import type { AIRecommendation } from '@/types/design-thinking'
|
||||||
|
|
||||||
|
interface RecommendationsWidgetProps {
|
||||||
|
recommendations: AIRecommendation[]
|
||||||
|
onAction?: (recommendation: AIRecommendation) => void
|
||||||
|
onDismiss?: (recommendationId: string) => void
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeConfig: Record<AIRecommendation['type'], { icon: typeof Sparkles; color: string; bg: string }> = {
|
||||||
|
action: { icon: Zap, color: 'text-blue-500', bg: 'bg-blue-500/10' },
|
||||||
|
warning: { icon: AlertTriangle, color: 'text-amber-500', bg: 'bg-amber-500/10' },
|
||||||
|
insight: { icon: Lightbulb, color: 'text-purple-500', bg: 'bg-purple-500/10' },
|
||||||
|
optimization: { icon: TrendingUp, color: 'text-emerald-500', bg: 'bg-emerald-500/10' },
|
||||||
|
}
|
||||||
|
|
||||||
|
const priorityColors: Record<AIRecommendation['priority'], string> = {
|
||||||
|
low: 'bg-muted text-muted-foreground',
|
||||||
|
medium: 'bg-amber-500/10 text-amber-600 border-amber-500/20',
|
||||||
|
high: 'bg-red-500/10 text-red-600 border-red-500/20',
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecommendationCard({
|
||||||
|
recommendation,
|
||||||
|
onAction,
|
||||||
|
onDismiss
|
||||||
|
}: {
|
||||||
|
recommendation: AIRecommendation
|
||||||
|
onAction?: (recommendation: AIRecommendation) => void
|
||||||
|
onDismiss?: (recommendationId: string) => void
|
||||||
|
}) {
|
||||||
|
const config = typeConfig[recommendation.type]
|
||||||
|
const Icon = config.icon
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layout
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 20, height: 0 }}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<div className={cn(
|
||||||
|
'relative p-3 rounded-lg border transition-all hover:shadow-sm',
|
||||||
|
config.bg,
|
||||||
|
'border-transparent hover:border-foreground/5'
|
||||||
|
)}>
|
||||||
|
{/* Dismiss button */}
|
||||||
|
{onDismiss && (
|
||||||
|
<button
|
||||||
|
onClick={() => onDismiss(recommendation.id)}
|
||||||
|
className="absolute top-2 right-2 p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity hover:bg-foreground/10"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className={cn('p-1.5 rounded-lg', config.bg)}>
|
||||||
|
<Icon className={cn('w-4 h-4', config.color)} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0 pr-6">
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<span className="text-sm font-medium">{recommendation.title}</span>
|
||||||
|
<Badge variant="outline" className={cn('text-[10px] px-1.5', priorityColors[recommendation.priority])}>
|
||||||
|
{recommendation.priority}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground mb-2">
|
||||||
|
{recommendation.description}
|
||||||
|
</p>
|
||||||
|
{recommendation.action_command && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs gap-1 -ml-2"
|
||||||
|
onClick={() => onAction?.(recommendation)}
|
||||||
|
>
|
||||||
|
<code className="bg-muted px-1 rounded">{recommendation.action_command}</code>
|
||||||
|
<ChevronRight className="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecommendationsWidget({
|
||||||
|
recommendations,
|
||||||
|
onAction,
|
||||||
|
onDismiss,
|
||||||
|
className
|
||||||
|
}: RecommendationsWidgetProps) {
|
||||||
|
const sortedRecommendations = [...recommendations].sort((a, b) => {
|
||||||
|
const priorityOrder = { high: 0, medium: 1, low: 2 }
|
||||||
|
return priorityOrder[a.priority] - priorityOrder[b.priority]
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={className}>
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-sm font-medium">
|
||||||
|
<Sparkles className="w-4 h-4 text-purple-500" />
|
||||||
|
AI Recommendations
|
||||||
|
{recommendations.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="ml-auto">
|
||||||
|
{recommendations.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 max-h-[400px] overflow-y-auto">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{sortedRecommendations.map((rec) => (
|
||||||
|
<RecommendationCard
|
||||||
|
key={rec.id}
|
||||||
|
recommendation={rec}
|
||||||
|
onAction={onAction}
|
||||||
|
onDismiss={onDismiss}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
{recommendations.length === 0 && (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<Sparkles className="w-8 h-8 text-muted-foreground/30 mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
No recommendations yet
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground/60">
|
||||||
|
AI insights will appear as you work
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
31
src/components/ui/tooltip.tsx
Normal file
31
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import * as React from 'react'
|
||||||
|
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const TooltipProvider = TooltipPrimitive.Provider
|
||||||
|
|
||||||
|
const Tooltip = TooltipPrimitive.Root
|
||||||
|
|
||||||
|
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||||
|
|
||||||
|
const TooltipContent = React.forwardRef<
|
||||||
|
React.ComponentRef<typeof TooltipPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
))
|
||||||
|
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
123
src/types/design-thinking.ts
Normal file
123
src/types/design-thinking.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
export const DESIGN_PHASES = ['empathize', 'define', 'ideate', 'prototype', 'test'] as const
|
||||||
|
export type DesignPhase = typeof DESIGN_PHASES[number]
|
||||||
|
|
||||||
|
export type PhaseStatus = 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'needs_review'
|
||||||
|
|
||||||
|
export interface PhaseConfig {
|
||||||
|
id: DesignPhase
|
||||||
|
icon: string
|
||||||
|
label: string
|
||||||
|
shortDescription: string
|
||||||
|
fullDescription: string
|
||||||
|
commands: string[]
|
||||||
|
color: string
|
||||||
|
questions: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const designPhases: PhaseConfig[] = [
|
||||||
|
{
|
||||||
|
id: 'empathize',
|
||||||
|
icon: '🎯',
|
||||||
|
label: 'Understand',
|
||||||
|
shortDescription: 'Learn about your users',
|
||||||
|
fullDescription: 'Research user needs, pain points, and behaviors',
|
||||||
|
commands: ['/research', '/ux', '/interview'],
|
||||||
|
color: 'purple',
|
||||||
|
questions: [
|
||||||
|
'Who are your users?',
|
||||||
|
'What problems do they face?',
|
||||||
|
'What do they need?'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'define',
|
||||||
|
icon: '📋',
|
||||||
|
label: 'Focus',
|
||||||
|
shortDescription: 'Define the problem',
|
||||||
|
fullDescription: 'Create clear problem statements and success criteria',
|
||||||
|
commands: ['/plan', '/roadmap', '/goal'],
|
||||||
|
color: 'blue',
|
||||||
|
questions: [
|
||||||
|
'What specific problem are we solving?',
|
||||||
|
'How will we measure success?',
|
||||||
|
'What are our constraints?'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ideate',
|
||||||
|
icon: '💡',
|
||||||
|
label: 'Explore',
|
||||||
|
shortDescription: 'Generate solutions',
|
||||||
|
fullDescription: 'Brainstorm ideas and evaluate options',
|
||||||
|
commands: ['/brainstorm', '/ideate', '/options'],
|
||||||
|
color: 'amber',
|
||||||
|
questions: [
|
||||||
|
'What are all possible solutions?',
|
||||||
|
'What are the trade-offs?',
|
||||||
|
'Which ideas should we explore?'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prototype',
|
||||||
|
icon: '🔨',
|
||||||
|
label: 'Build',
|
||||||
|
shortDescription: 'Create & deploy',
|
||||||
|
fullDescription: 'Build working prototypes and ship features',
|
||||||
|
commands: ['/build', '/deploy', '/ui'],
|
||||||
|
color: 'orange',
|
||||||
|
questions: [
|
||||||
|
'What should we build first?',
|
||||||
|
'How can we test quickly?',
|
||||||
|
'What is the minimum viable feature?'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'test',
|
||||||
|
icon: '✅',
|
||||||
|
label: 'Validate',
|
||||||
|
shortDescription: 'Test & review',
|
||||||
|
fullDescription: 'Validate with users and gather feedback',
|
||||||
|
commands: ['/test', '/review', '/feedback'],
|
||||||
|
color: 'emerald',
|
||||||
|
questions: [
|
||||||
|
'Does this solve the problem?',
|
||||||
|
'What feedback do users have?',
|
||||||
|
'What needs improvement?'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export interface BacklogItem {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
phase: DesignPhase
|
||||||
|
priority_score: number
|
||||||
|
user_value: number
|
||||||
|
time_criticality: number
|
||||||
|
risk_reduction: number
|
||||||
|
effort: number
|
||||||
|
status: 'backlog' | 'ready' | 'in_progress' | 'done' | 'blocked'
|
||||||
|
depends_on?: string[]
|
||||||
|
assigned_to?: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectHealth {
|
||||||
|
overall: number
|
||||||
|
velocity: number
|
||||||
|
blockers: number
|
||||||
|
overdue: number
|
||||||
|
completion_rate: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIRecommendation {
|
||||||
|
id: string
|
||||||
|
type: 'action' | 'warning' | 'insight' | 'optimization'
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
priority: 'low' | 'medium' | 'high'
|
||||||
|
related_items?: string[]
|
||||||
|
action_command?: string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user