prjct-cli 0.11.4 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/bin/serve.js +22 -6
- package/core/__tests__/utils/date-helper.test.js +416 -0
- package/core/agentic/agent-router.js +30 -18
- package/core/agentic/command-executor.js +20 -24
- package/core/agentic/context-builder.js +7 -8
- package/core/agentic/memory-system.js +14 -19
- package/core/agentic/prompt-builder.js +41 -27
- package/core/agentic/template-loader.js +8 -2
- package/core/infrastructure/agent-detector.js +7 -4
- package/core/infrastructure/migrator.js +10 -13
- package/core/infrastructure/session-manager.js +10 -10
- package/package.json +1 -1
- package/packages/web/app/project/[id]/stats/page.tsx +102 -343
- package/packages/web/components/stats/ActivityTimeline.tsx +201 -0
- package/packages/web/components/stats/AgentsCard.tsx +56 -0
- package/packages/web/components/stats/BentoCard.tsx +88 -0
- package/packages/web/components/stats/BentoGrid.tsx +22 -0
- package/packages/web/components/stats/EmptyState.tsx +67 -0
- package/packages/web/components/stats/HeroSection.tsx +172 -0
- package/packages/web/components/stats/IdeasCard.tsx +59 -0
- package/packages/web/components/stats/NowCard.tsx +71 -0
- package/packages/web/components/stats/ProgressRing.tsx +74 -0
- package/packages/web/components/stats/QueueCard.tsx +58 -0
- package/packages/web/components/stats/RoadmapCard.tsx +97 -0
- package/packages/web/components/stats/ShipsCard.tsx +70 -0
- package/packages/web/components/stats/SparklineChart.tsx +44 -0
- package/packages/web/components/stats/StreakCard.tsx +59 -0
- package/packages/web/components/stats/VelocityCard.tsx +60 -0
- package/packages/web/components/stats/index.ts +17 -0
- package/packages/web/components/ui/tooltip.tsx +2 -2
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { BentoCard } from './BentoCard'
|
|
4
|
+
import { EmptyState } from './EmptyState'
|
|
5
|
+
import { Target, Clock } from 'lucide-react'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
|
|
8
|
+
interface CurrentTask {
|
|
9
|
+
task: string
|
|
10
|
+
duration?: string
|
|
11
|
+
startedAt?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface NowCardProps {
|
|
15
|
+
currentTask: CurrentTask | null
|
|
16
|
+
className?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function NowCard({ currentTask, className }: NowCardProps) {
|
|
20
|
+
return (
|
|
21
|
+
<BentoCard
|
|
22
|
+
size="2x2"
|
|
23
|
+
title="Now"
|
|
24
|
+
icon={Target}
|
|
25
|
+
accentColor={currentTask ? 'warning' : 'default'}
|
|
26
|
+
className={className}
|
|
27
|
+
>
|
|
28
|
+
{currentTask ? (
|
|
29
|
+
<div className="flex flex-col h-full">
|
|
30
|
+
{/* Status indicator */}
|
|
31
|
+
<div className="flex items-center gap-2 mb-3">
|
|
32
|
+
<div className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
|
|
33
|
+
<span className="text-[10px] font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-wider">
|
|
34
|
+
Working
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Task name */}
|
|
39
|
+
<p className="text-xl font-semibold leading-tight flex-1">
|
|
40
|
+
{currentTask.task}
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
{/* Duration */}
|
|
44
|
+
{currentTask.duration && (
|
|
45
|
+
<div className="flex items-center gap-2 mt-4 text-muted-foreground">
|
|
46
|
+
<Clock className="h-3.5 w-3.5" />
|
|
47
|
+
<span className="text-sm">{currentTask.duration} elapsed</span>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{/* Progress visualization - simple bar */}
|
|
52
|
+
<div className="mt-4">
|
|
53
|
+
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
|
|
54
|
+
<div
|
|
55
|
+
className="h-full bg-amber-500 rounded-full animate-pulse"
|
|
56
|
+
style={{ width: '60%' }}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
) : (
|
|
62
|
+
<EmptyState
|
|
63
|
+
icon={Target}
|
|
64
|
+
title="No active task"
|
|
65
|
+
description="Start working on something"
|
|
66
|
+
command="/p:now"
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
</BentoCard>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
interface ProgressRingProps {
|
|
6
|
+
value: number // 0-100
|
|
7
|
+
size?: 'sm' | 'md' | 'lg' | 'xl'
|
|
8
|
+
showValue?: boolean
|
|
9
|
+
strokeWidth?: number
|
|
10
|
+
className?: string
|
|
11
|
+
accentColor?: 'default' | 'success' | 'warning' | 'destructive'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sizes = {
|
|
15
|
+
sm: { container: 'h-8 w-8', text: 'text-[10px]', viewBox: 36, radius: 14 },
|
|
16
|
+
md: { container: 'h-12 w-12', text: 'text-xs', viewBox: 36, radius: 14 },
|
|
17
|
+
lg: { container: 'h-16 w-16', text: 'text-sm', viewBox: 36, radius: 14 },
|
|
18
|
+
xl: { container: 'h-20 w-20', text: 'text-base', viewBox: 36, radius: 14 },
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const colorStyles = {
|
|
22
|
+
default: 'text-foreground',
|
|
23
|
+
success: 'text-emerald-500',
|
|
24
|
+
warning: 'text-amber-500',
|
|
25
|
+
destructive: 'text-destructive',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ProgressRing({
|
|
29
|
+
value,
|
|
30
|
+
size = 'md',
|
|
31
|
+
showValue = true,
|
|
32
|
+
strokeWidth = 3,
|
|
33
|
+
className,
|
|
34
|
+
accentColor = 'default',
|
|
35
|
+
}: ProgressRingProps) {
|
|
36
|
+
const { container, text, viewBox, radius } = sizes[size]
|
|
37
|
+
const circumference = 2 * Math.PI * radius
|
|
38
|
+
const strokeDashoffset = circumference - (value / 100) * circumference
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className={cn('relative', container, className)}>
|
|
42
|
+
<svg className="h-full w-full -rotate-90" viewBox={`0 0 ${viewBox} ${viewBox}`}>
|
|
43
|
+
{/* Background ring */}
|
|
44
|
+
<circle
|
|
45
|
+
cx={viewBox / 2}
|
|
46
|
+
cy={viewBox / 2}
|
|
47
|
+
r={radius}
|
|
48
|
+
fill="none"
|
|
49
|
+
stroke="currentColor"
|
|
50
|
+
strokeWidth={strokeWidth}
|
|
51
|
+
className="text-foreground/10"
|
|
52
|
+
/>
|
|
53
|
+
{/* Progress ring */}
|
|
54
|
+
<circle
|
|
55
|
+
cx={viewBox / 2}
|
|
56
|
+
cy={viewBox / 2}
|
|
57
|
+
r={radius}
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeWidth={strokeWidth}
|
|
61
|
+
strokeDasharray={circumference}
|
|
62
|
+
strokeDashoffset={strokeDashoffset}
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
className={cn('transition-all duration-700 ease-out', colorStyles[accentColor])}
|
|
65
|
+
/>
|
|
66
|
+
</svg>
|
|
67
|
+
{showValue && (
|
|
68
|
+
<span className={cn('absolute inset-0 flex items-center justify-center font-bold tabular-nums', text)}>
|
|
69
|
+
{value}
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { BentoCard } from './BentoCard'
|
|
4
|
+
import { EmptyState } from './EmptyState'
|
|
5
|
+
import { ListTodo } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
interface QueueItem {
|
|
8
|
+
task: string
|
|
9
|
+
priority?: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface QueueCardProps {
|
|
13
|
+
queue: QueueItem[]
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function QueueCard({ queue, className }: QueueCardProps) {
|
|
18
|
+
const displayItems = queue.slice(0, 5)
|
|
19
|
+
const remaining = queue.length - 5
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<BentoCard
|
|
23
|
+
size="1x2"
|
|
24
|
+
title="Queue"
|
|
25
|
+
icon={ListTodo}
|
|
26
|
+
count={queue.length}
|
|
27
|
+
className={className}
|
|
28
|
+
>
|
|
29
|
+
{queue.length === 0 ? (
|
|
30
|
+
<EmptyState
|
|
31
|
+
icon={ListTodo}
|
|
32
|
+
title="Queue empty"
|
|
33
|
+
description="Plan your next tasks"
|
|
34
|
+
command="/p:next"
|
|
35
|
+
compact
|
|
36
|
+
/>
|
|
37
|
+
) : (
|
|
38
|
+
<div className="space-y-2">
|
|
39
|
+
{displayItems.map((item, i) => (
|
|
40
|
+
<div key={i} className="flex items-start gap-2 group">
|
|
41
|
+
<span className="text-[10px] font-medium text-muted-foreground w-4 shrink-0 pt-0.5 tabular-nums">
|
|
42
|
+
{i + 1}
|
|
43
|
+
</span>
|
|
44
|
+
<p className="text-sm leading-tight truncate flex-1 group-hover:text-foreground transition-colors">
|
|
45
|
+
{item.task}
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
{remaining > 0 && (
|
|
50
|
+
<p className="text-[10px] text-muted-foreground mt-2 pl-6">
|
|
51
|
+
+{remaining} more
|
|
52
|
+
</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</BentoCard>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { BentoCard } from './BentoCard'
|
|
4
|
+
import { EmptyState } from './EmptyState'
|
|
5
|
+
import { Map } from 'lucide-react'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
|
|
8
|
+
interface RoadmapPhase {
|
|
9
|
+
name: string
|
|
10
|
+
progress: number
|
|
11
|
+
features?: { name: string; status: string }[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface RoadmapData {
|
|
15
|
+
phases: RoadmapPhase[]
|
|
16
|
+
progress: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface RoadmapCardProps {
|
|
20
|
+
roadmap: RoadmapData | null
|
|
21
|
+
className?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function RoadmapCard({ roadmap, className }: RoadmapCardProps) {
|
|
25
|
+
const hasPhases = roadmap?.phases && roadmap.phases.length > 0
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<BentoCard
|
|
29
|
+
size="2x2"
|
|
30
|
+
title="Roadmap"
|
|
31
|
+
icon={Map}
|
|
32
|
+
count={hasPhases ? `${roadmap.progress}%` : undefined}
|
|
33
|
+
className={className}
|
|
34
|
+
>
|
|
35
|
+
{!hasPhases ? (
|
|
36
|
+
<EmptyState
|
|
37
|
+
icon={Map}
|
|
38
|
+
title="No roadmap yet"
|
|
39
|
+
description="Plan your features"
|
|
40
|
+
command="/p:feature"
|
|
41
|
+
/>
|
|
42
|
+
) : (
|
|
43
|
+
<div className="flex flex-col h-full">
|
|
44
|
+
{/* Overall progress */}
|
|
45
|
+
<div className="mb-4">
|
|
46
|
+
<div className="flex items-center justify-between text-xs mb-1.5">
|
|
47
|
+
<span className="text-muted-foreground">Overall progress</span>
|
|
48
|
+
<span className="font-bold tabular-nums">{roadmap.progress}%</span>
|
|
49
|
+
</div>
|
|
50
|
+
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
|
51
|
+
<div
|
|
52
|
+
className={cn(
|
|
53
|
+
'h-full rounded-full transition-all duration-500',
|
|
54
|
+
roadmap.progress === 100 ? 'bg-emerald-500' : 'bg-foreground'
|
|
55
|
+
)}
|
|
56
|
+
style={{ width: `${roadmap.progress}%` }}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Phases */}
|
|
62
|
+
<div className="space-y-3 flex-1">
|
|
63
|
+
{roadmap.phases
|
|
64
|
+
.filter(p => (p.features || []).length > 0)
|
|
65
|
+
.slice(0, 4)
|
|
66
|
+
.map((phase) => (
|
|
67
|
+
<div key={phase.name}>
|
|
68
|
+
<div className="flex items-center justify-between text-xs mb-1">
|
|
69
|
+
<span className="font-medium truncate">{phase.name}</span>
|
|
70
|
+
<span className="text-muted-foreground tabular-nums shrink-0 ml-2">
|
|
71
|
+
{phase.progress}%
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
|
|
75
|
+
<div
|
|
76
|
+
className={cn(
|
|
77
|
+
'h-full rounded-full transition-all duration-300',
|
|
78
|
+
phase.progress === 100 ? 'bg-emerald-500' : 'bg-foreground/70'
|
|
79
|
+
)}
|
|
80
|
+
style={{ width: `${phase.progress}%` }}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
))}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{/* Feature count */}
|
|
88
|
+
{roadmap.phases.length > 0 && (
|
|
89
|
+
<p className="text-[10px] text-muted-foreground mt-3">
|
|
90
|
+
{roadmap.phases.reduce((acc, p) => acc + (p.features?.length || 0), 0)} features across {roadmap.phases.length} phases
|
|
91
|
+
</p>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</BentoCard>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { BentoCard } from './BentoCard'
|
|
4
|
+
import { EmptyState } from './EmptyState'
|
|
5
|
+
import { Rocket } from 'lucide-react'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
7
|
+
|
|
8
|
+
interface Ship {
|
|
9
|
+
name: string
|
|
10
|
+
date: string
|
|
11
|
+
version?: string
|
|
12
|
+
duration?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ShipsCardProps {
|
|
16
|
+
ships: Ship[]
|
|
17
|
+
totalShips?: number
|
|
18
|
+
className?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatShipDate(dateString: string): string {
|
|
22
|
+
const date = new Date(dateString)
|
|
23
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ShipsCard({ ships, totalShips = 0, className }: ShipsCardProps) {
|
|
27
|
+
const displayShips = ships.slice(0, 4)
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<BentoCard
|
|
31
|
+
size="1x2"
|
|
32
|
+
title="Ships"
|
|
33
|
+
icon={Rocket}
|
|
34
|
+
count={totalShips}
|
|
35
|
+
accentColor={ships.length > 0 ? 'success' : 'default'}
|
|
36
|
+
className={className}
|
|
37
|
+
>
|
|
38
|
+
{ships.length === 0 ? (
|
|
39
|
+
<EmptyState
|
|
40
|
+
icon={Rocket}
|
|
41
|
+
title="Nothing shipped yet"
|
|
42
|
+
description="Ship your first feature"
|
|
43
|
+
command="/p:ship"
|
|
44
|
+
compact
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<div className="space-y-3">
|
|
48
|
+
{displayShips.map((ship, i) => (
|
|
49
|
+
<div key={i} className="group">
|
|
50
|
+
<div className="flex items-center gap-2">
|
|
51
|
+
{ship.version && (
|
|
52
|
+
<Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono shrink-0">
|
|
53
|
+
{ship.version}
|
|
54
|
+
</Badge>
|
|
55
|
+
)}
|
|
56
|
+
<p className="text-sm font-medium truncate group-hover:text-foreground transition-colors">
|
|
57
|
+
{ship.name}
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
<p className="text-[10px] text-muted-foreground mt-0.5">
|
|
61
|
+
{formatShipDate(ship.date)}
|
|
62
|
+
{ship.duration && ` · ${ship.duration}`}
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</BentoCard>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
|
|
4
|
+
|
|
5
|
+
interface SparklineChartProps {
|
|
6
|
+
data: number[]
|
|
7
|
+
color?: string
|
|
8
|
+
height?: number
|
|
9
|
+
showArea?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SparklineChart({
|
|
13
|
+
data,
|
|
14
|
+
color = 'currentColor',
|
|
15
|
+
height = 32,
|
|
16
|
+
showArea = true,
|
|
17
|
+
}: SparklineChartProps) {
|
|
18
|
+
const chartData = data.map((value, index) => ({ index, value }))
|
|
19
|
+
|
|
20
|
+
if (data.length === 0) {
|
|
21
|
+
return <div style={{ height }} className="w-full" />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
26
|
+
<AreaChart data={chartData} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
|
|
27
|
+
<defs>
|
|
28
|
+
<linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
|
|
29
|
+
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
|
|
30
|
+
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
31
|
+
</linearGradient>
|
|
32
|
+
</defs>
|
|
33
|
+
<Area
|
|
34
|
+
type="monotone"
|
|
35
|
+
dataKey="value"
|
|
36
|
+
stroke={color}
|
|
37
|
+
strokeWidth={1.5}
|
|
38
|
+
fill={showArea ? 'url(#sparklineGradient)' : 'none'}
|
|
39
|
+
isAnimationActive={false}
|
|
40
|
+
/>
|
|
41
|
+
</AreaChart>
|
|
42
|
+
</ResponsiveContainer>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { BentoCard } from './BentoCard'
|
|
4
|
+
import { Flame } from 'lucide-react'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
interface StreakCardProps {
|
|
8
|
+
streak: number
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function StreakCard({ streak, className }: StreakCardProps) {
|
|
13
|
+
const isHot = streak >= 3
|
|
14
|
+
const isOnFire = streak >= 7
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<BentoCard
|
|
18
|
+
size="1x1"
|
|
19
|
+
title="Streak"
|
|
20
|
+
icon={Flame}
|
|
21
|
+
accentColor={isOnFire ? 'warning' : 'default'}
|
|
22
|
+
className={className}
|
|
23
|
+
>
|
|
24
|
+
<div className="flex flex-col h-full justify-between">
|
|
25
|
+
<div className="flex items-center gap-3">
|
|
26
|
+
<Flame
|
|
27
|
+
className={cn(
|
|
28
|
+
'h-8 w-8 transition-colors',
|
|
29
|
+
isOnFire ? 'text-orange-500' : isHot ? 'text-amber-500' : 'text-muted-foreground'
|
|
30
|
+
)}
|
|
31
|
+
/>
|
|
32
|
+
<div>
|
|
33
|
+
<p className="text-3xl font-bold tabular-nums">{streak}</p>
|
|
34
|
+
<p className="text-xs text-muted-foreground">day{streak !== 1 ? 's' : ''}</p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Visual streak indicator - dots */}
|
|
39
|
+
<div className="flex gap-1 mt-3">
|
|
40
|
+
{Array.from({ length: 7 }).map((_, i) => (
|
|
41
|
+
<div
|
|
42
|
+
key={i}
|
|
43
|
+
className={cn(
|
|
44
|
+
'h-1.5 flex-1 rounded-full transition-colors',
|
|
45
|
+
i < streak
|
|
46
|
+
? isOnFire
|
|
47
|
+
? 'bg-orange-500'
|
|
48
|
+
: isHot
|
|
49
|
+
? 'bg-amber-500'
|
|
50
|
+
: 'bg-foreground'
|
|
51
|
+
: 'bg-muted'
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</BentoCard>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { BentoCard } from './BentoCard'
|
|
4
|
+
import { SparklineChart } from './SparklineChart'
|
|
5
|
+
import { Zap, TrendingUp, TrendingDown } from 'lucide-react'
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
|
|
8
|
+
interface VelocityCardProps {
|
|
9
|
+
tasksPerDay: number
|
|
10
|
+
weeklyData?: number[]
|
|
11
|
+
change?: number
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function VelocityCard({
|
|
16
|
+
tasksPerDay,
|
|
17
|
+
weeklyData = [],
|
|
18
|
+
change = 0,
|
|
19
|
+
className,
|
|
20
|
+
}: VelocityCardProps) {
|
|
21
|
+
return (
|
|
22
|
+
<BentoCard
|
|
23
|
+
size="1x1"
|
|
24
|
+
title="Velocity"
|
|
25
|
+
icon={Zap}
|
|
26
|
+
className={className}
|
|
27
|
+
>
|
|
28
|
+
<div className="flex flex-col h-full justify-between">
|
|
29
|
+
<div>
|
|
30
|
+
<p className="text-3xl font-bold tabular-nums">{tasksPerDay}</p>
|
|
31
|
+
<p className="text-xs text-muted-foreground">tasks/day</p>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{weeklyData.length > 0 && (
|
|
35
|
+
<div className="mt-2">
|
|
36
|
+
<SparklineChart data={weeklyData} height={28} />
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
{change !== 0 && (
|
|
41
|
+
<div className="flex items-center gap-1 mt-2">
|
|
42
|
+
{change >= 0 ? (
|
|
43
|
+
<TrendingUp className="h-3 w-3 text-emerald-500" />
|
|
44
|
+
) : (
|
|
45
|
+
<TrendingDown className="h-3 w-3 text-muted-foreground" />
|
|
46
|
+
)}
|
|
47
|
+
<span
|
|
48
|
+
className={cn(
|
|
49
|
+
'text-xs font-medium',
|
|
50
|
+
change >= 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{change >= 0 ? '+' : ''}{change}%
|
|
54
|
+
</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</BentoCard>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { BentoCard, BentoCardSkeleton } from './BentoCard'
|
|
2
|
+
export type { BentoSize, BentoCardProps } from './BentoCard'
|
|
3
|
+
|
|
4
|
+
export { BentoGrid } from './BentoGrid'
|
|
5
|
+
export { SparklineChart } from './SparklineChart'
|
|
6
|
+
export { ProgressRing } from './ProgressRing'
|
|
7
|
+
export { EmptyState } from './EmptyState'
|
|
8
|
+
export { HeroSection } from './HeroSection'
|
|
9
|
+
export { NowCard } from './NowCard'
|
|
10
|
+
export { VelocityCard } from './VelocityCard'
|
|
11
|
+
export { StreakCard } from './StreakCard'
|
|
12
|
+
export { QueueCard } from './QueueCard'
|
|
13
|
+
export { ShipsCard } from './ShipsCard'
|
|
14
|
+
export { IdeasCard } from './IdeasCard'
|
|
15
|
+
export { AgentsCard } from './AgentsCard'
|
|
16
|
+
export { RoadmapCard } from './RoadmapCard'
|
|
17
|
+
export { ActivityTimeline } from './ActivityTimeline'
|
|
@@ -46,13 +46,13 @@ function TooltipContent({
|
|
|
46
46
|
data-slot="tooltip-content"
|
|
47
47
|
sideOffset={sideOffset}
|
|
48
48
|
className={cn(
|
|
49
|
-
"bg-
|
|
49
|
+
"bg-popover text-popover-foreground border shadow-md 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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
|
50
50
|
className
|
|
51
51
|
)}
|
|
52
52
|
{...props}
|
|
53
53
|
>
|
|
54
54
|
{children}
|
|
55
|
-
<TooltipPrimitive.Arrow className="bg-
|
|
55
|
+
<TooltipPrimitive.Arrow className="bg-popover fill-popover z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
|
56
56
|
</TooltipPrimitive.Content>
|
|
57
57
|
</TooltipPrimitive.Portal>
|
|
58
58
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|