prjct-cli 0.13.3 → 0.15.0

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. package/templates/commands/workflow.md +0 -12
@@ -1,72 +1,125 @@
1
- import { BentoCard } from '@/components/BentoCard'
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import Link from 'next/link'
2
5
  import { EmptyState } from '@/components/EmptyState'
3
- import { ListTodo, Bot } from 'lucide-react'
6
+ import { ExpandButton } from '@/components/ExpandButton'
7
+ import { ListTodo, Bot, Play, X } from 'lucide-react'
4
8
  import { cn } from '@/lib/utils'
5
9
  import { getPriorityColor } from './QueueCard.utils'
6
10
  import type { QueueCardProps } from './QueueCard.types'
7
11
 
8
- export function QueueCard({ queue, className }: QueueCardProps) {
9
- const displayItems = queue.slice(0, 5)
10
- const remaining = queue.length - 5
12
+ const COLLAPSED_LIMIT = 10
13
+ const EXPANDED_LIMIT = 50
14
+
15
+ export function QueueCard({ queue, codeHref, className }: QueueCardProps) {
16
+ const [expanded, setExpanded] = useState(false)
17
+ const limit = expanded ? EXPANDED_LIMIT : COLLAPSED_LIMIT
18
+ const displayItems = queue.slice(0, limit)
19
+ const hasMore = queue.length > limit
11
20
 
12
21
  return (
13
- <BentoCard
14
- size="1x2"
15
- title="Queue"
16
- icon={ListTodo}
17
- count={queue.length}
18
- className={className}
19
- >
22
+ <div className={cn(
23
+ 'relative overflow-hidden rounded-xl border bg-card p-4',
24
+ className
25
+ )}>
26
+ <div className="flex items-center justify-between mb-3">
27
+ <div className="flex items-center gap-2">
28
+ <ListTodo className="h-4 w-4 text-muted-foreground" />
29
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
30
+ Queue
31
+ </span>
32
+ </div>
33
+ <span className="text-xs font-medium text-muted-foreground tabular-nums">
34
+ {queue.length} tasks
35
+ </span>
36
+ </div>
37
+
20
38
  {queue.length === 0 ? (
21
39
  <EmptyState
22
40
  icon={ListTodo}
23
41
  title="Queue empty"
24
42
  description="Plan your next tasks"
25
43
  command="/p:next"
44
+ href={codeHref}
26
45
  compact
27
46
  />
28
47
  ) : (
29
- <div className="space-y-2">
48
+ <div className="space-y-1.5">
30
49
  {displayItems.map((item, i) => {
31
50
  const priorityColor = getPriorityColor(item.priority)
51
+ const startHref = codeHref
52
+ ? `${codeHref}?cmd=${encodeURIComponent(`p. now "${item.task}"`)}`
53
+ : undefined
54
+ const deleteHref = codeHref
55
+ ? `${codeHref}?cmd=${encodeURIComponent(`p. queue remove ${i + 1}`)}`
56
+ : undefined
57
+
32
58
  return (
33
- <div key={i} className="flex items-start gap-2 group">
59
+ <div
60
+ key={i}
61
+ className="flex items-start gap-2 group py-1.5 hover:bg-muted/50 rounded px-1 -mx-1"
62
+ >
34
63
  <span className={cn(
35
- "text-[10px] font-medium w-4 shrink-0 pt-0.5 tabular-nums",
64
+ "text-xs font-bold w-5 shrink-0 pt-0.5 tabular-nums",
36
65
  priorityColor
37
66
  )}>
38
- {i + 1}
67
+ {i + 1}.
39
68
  </span>
40
69
  <div className="flex-1 min-w-0">
41
- <p className="text-sm leading-tight truncate group-hover:text-foreground transition-colors">
70
+ <p className="text-sm leading-tight">
42
71
  {item.task}
43
72
  </p>
44
73
  {(item.suggestedAgent || item.estimatedDuration) && (
45
74
  <div className="flex items-center gap-2 mt-0.5">
46
75
  {item.suggestedAgent && (
47
- <span className="inline-flex items-center gap-0.5 text-[9px] text-muted-foreground">
48
- <Bot className="h-2.5 w-2.5" />
76
+ <span className="inline-flex items-center gap-0.5 text-xs text-muted-foreground">
77
+ <Bot className="h-3 w-3" />
49
78
  {item.suggestedAgent}
50
79
  </span>
51
80
  )}
52
81
  {item.estimatedDuration && (
53
- <span className="text-[9px] text-muted-foreground">
82
+ <span className="text-xs text-muted-foreground">
54
83
  ~{item.estimatedDuration}
55
84
  </span>
56
85
  )}
57
86
  </div>
58
87
  )}
59
88
  </div>
89
+ {/* Always visible action buttons */}
90
+ <div className="flex items-center gap-1 shrink-0">
91
+ {startHref && (
92
+ <Link
93
+ href={startHref}
94
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
95
+ title="Start now"
96
+ >
97
+ <Play className="h-3.5 w-3.5" />
98
+ </Link>
99
+ )}
100
+ {deleteHref && (
101
+ <Link
102
+ href={deleteHref}
103
+ className="p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
104
+ title="Remove from queue"
105
+ >
106
+ <X className="h-3.5 w-3.5" />
107
+ </Link>
108
+ )}
109
+ </div>
60
110
  </div>
61
111
  )
62
112
  })}
63
- {remaining > 0 && (
64
- <p className="text-[10px] text-muted-foreground mt-2 pl-6">
65
- +{remaining} more
66
- </p>
113
+ {(hasMore || expanded) && queue.length > COLLAPSED_LIMIT && (
114
+ <ExpandButton
115
+ expanded={expanded}
116
+ totalCount={queue.length}
117
+ collapsedLimit={COLLAPSED_LIMIT}
118
+ onToggle={() => setExpanded(!expanded)}
119
+ />
67
120
  )}
68
121
  </div>
69
122
  )}
70
- </BentoCard>
123
+ </div>
71
124
  )
72
125
  }
@@ -7,5 +7,6 @@ export interface QueueItem {
7
7
 
8
8
  export interface QueueCardProps {
9
9
  queue: QueueItem[]
10
+ codeHref?: string
10
11
  className?: string
11
12
  }
@@ -1,9 +1,9 @@
1
1
  export function getPriorityColor(priority?: 'low' | 'medium' | 'high' | 'critical' | number): string {
2
2
  if (typeof priority === 'string') {
3
3
  const colors: Record<string, string> = {
4
- critical: 'text-red-500',
5
- high: 'text-amber-500',
6
- medium: 'text-blue-500',
4
+ critical: 'text-foreground font-bold',
5
+ high: 'text-foreground',
6
+ medium: 'text-muted-foreground',
7
7
  low: 'text-muted-foreground',
8
8
  }
9
9
  return colors[priority] ?? 'text-muted-foreground'
@@ -0,0 +1,72 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { AlertCircle, ChevronRight, Clock, MessageSquare } from 'lucide-react'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import { cn } from '@/lib/utils'
7
+ import type { RecoverCardProps } from './RecoverCard.types'
8
+
9
+ export function RecoverCard({ abandonedSessions, codeHref, className }: RecoverCardProps) {
10
+ if (abandonedSessions.length === 0) return null
11
+
12
+ return (
13
+ <div className={cn(
14
+ 'relative overflow-hidden rounded-xl border border-yellow-500/30 bg-yellow-500/5 p-4 min-w-0 max-w-full',
15
+ className
16
+ )}>
17
+ <div className="flex items-center justify-between mb-3 min-w-0">
18
+ <div className="flex items-center gap-2 min-w-0">
19
+ <AlertCircle className="h-4 w-4 text-yellow-500 shrink-0" />
20
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-yellow-600 dark:text-yellow-500 truncate">
21
+ Recover
22
+ </span>
23
+ </div>
24
+ <Badge variant="outline" className="text-yellow-600 dark:text-yellow-500 border-yellow-500/50 shrink-0">
25
+ {abandonedSessions.length}
26
+ </Badge>
27
+ </div>
28
+
29
+ <div className="space-y-2 min-w-0 max-w-full">
30
+ {abandonedSessions.slice(0, 3).map((session) => (
31
+ <Link
32
+ key={session.id}
33
+ href={`${codeHref}?cmd=p.%20recover`}
34
+ className="block py-2 px-2 -mx-2 hover:bg-yellow-500/10 rounded-lg transition-colors group"
35
+ >
36
+ <div className="flex items-start justify-between gap-2">
37
+ <div className="flex-1 min-w-0">
38
+ <p className="text-sm font-medium leading-tight truncate group-hover:text-foreground transition-colors">
39
+ {session.task}
40
+ </p>
41
+ <div className="flex items-center gap-2 mt-1 text-xs text-muted-foreground">
42
+ <Clock className="h-3 w-3 shrink-0" />
43
+ <span>{session.hoursAgo}h ago</span>
44
+ {session.projectName && (
45
+ <>
46
+ <span className="text-muted-foreground/50">|</span>
47
+ <span className="truncate">{session.projectName}</span>
48
+ </>
49
+ )}
50
+ </div>
51
+ {session.prompt && (
52
+ <div className="flex items-start gap-1.5 mt-1.5">
53
+ <MessageSquare className="h-3 w-3 text-muted-foreground/70 shrink-0 mt-0.5" />
54
+ <p className="text-xs text-muted-foreground/70 italic line-clamp-2">
55
+ &quot;{session.prompt.slice(0, 80)}{session.prompt.length > 80 ? '...' : ''}&quot;
56
+ </p>
57
+ </div>
58
+ )}
59
+ </div>
60
+ <ChevronRight className="h-4 w-4 text-muted-foreground/50 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 mt-1" />
61
+ </div>
62
+ </Link>
63
+ ))}
64
+ {abandonedSessions.length > 3 && (
65
+ <p className="text-xs text-yellow-600/70 dark:text-yellow-500/70 text-center pt-1">
66
+ +{abandonedSessions.length - 3} more abandoned sessions
67
+ </p>
68
+ )}
69
+ </div>
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,16 @@
1
+ export interface AbandonedSession {
2
+ id: string
3
+ task: string
4
+ projectId: string
5
+ projectName?: string
6
+ startedAt: string
7
+ lastActivity?: string
8
+ hoursAgo: number
9
+ prompt?: string
10
+ }
11
+
12
+ export interface RecoverCardProps {
13
+ abandonedSessions: AbandonedSession[]
14
+ codeHref: string
15
+ className?: string
16
+ }
@@ -0,0 +1,2 @@
1
+ export { RecoverCard } from './RecoverCard'
2
+ export type { RecoverCardProps, AbandonedSession } from './RecoverCard.types'
@@ -1,77 +1,145 @@
1
- import { BentoCard } from '@/components/BentoCard'
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
2
4
  import { EmptyState } from '@/components/EmptyState'
3
- import { Map } from 'lucide-react'
5
+ import { Map, ChevronRight, CheckCircle2, Circle } from 'lucide-react'
4
6
  import { cn } from '@/lib/utils'
5
7
  import type { RoadmapCardProps } from './RoadmapCard.types'
6
8
 
7
- export function RoadmapCard({ roadmap, className }: RoadmapCardProps) {
9
+ // Traffic light colors for progress: green=100%, yellow=50-99%, red=<50%
10
+ function getProgressColor(progress: number): string {
11
+ if (progress >= 100) return 'bg-emerald-500'
12
+ if (progress >= 50) return 'bg-amber-500'
13
+ return 'bg-red-500'
14
+ }
15
+
16
+ export function RoadmapCard({ roadmap, codeHref, className }: RoadmapCardProps) {
8
17
  const hasPhases = roadmap?.phases && roadmap.phases.length > 0
9
18
 
10
19
  return (
11
- <BentoCard
12
- size="2x2"
13
- title="Roadmap"
14
- icon={Map}
15
- count={hasPhases ? `${roadmap.progress}%` : undefined}
16
- className={className}
17
- >
20
+ <div className={cn(
21
+ 'relative overflow-hidden rounded-xl border bg-card p-4',
22
+ className
23
+ )}>
24
+ <div className="flex items-center justify-between mb-3">
25
+ <div className="flex items-center gap-2">
26
+ <Map className="h-4 w-4 text-muted-foreground" />
27
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
28
+ Roadmap
29
+ </span>
30
+ </div>
31
+ {hasPhases && (
32
+ <span className="text-xs font-bold tabular-nums">
33
+ {roadmap.progress}%
34
+ </span>
35
+ )}
36
+ </div>
37
+
18
38
  {!hasPhases ? (
19
39
  <EmptyState
20
40
  icon={Map}
21
41
  title="No roadmap yet"
22
42
  description="Plan your features"
23
43
  command="/p:feature"
44
+ href={codeHref}
24
45
  />
25
46
  ) : (
26
- <div className="flex flex-col h-full">
27
- <div className="mb-4">
28
- <div className="flex items-center justify-between text-xs mb-1.5">
29
- <span className="text-muted-foreground">Overall progress</span>
30
- <span className="font-bold tabular-nums">{roadmap.progress}%</span>
47
+ <div className="space-y-4">
48
+ {/* Overall progress */}
49
+ <div>
50
+ <div className="flex items-center justify-between mb-2">
51
+ <span className="text-xs text-muted-foreground">Overall progress</span>
52
+ <span className="text-sm font-bold tabular-nums">{roadmap.progress}%</span>
31
53
  </div>
32
- <div className="h-2 bg-muted rounded-full overflow-hidden">
54
+ <div className="h-3 bg-muted rounded-full overflow-hidden">
33
55
  <div
34
- className={cn(
35
- 'h-full rounded-full transition-all duration-500',
36
- roadmap.progress === 100 ? 'bg-emerald-500' : 'bg-foreground'
37
- )}
56
+ className={cn("h-full rounded-full transition-all duration-500", getProgressColor(roadmap.progress))}
38
57
  style={{ width: `${roadmap.progress}%` }}
39
58
  />
40
59
  </div>
41
60
  </div>
42
61
 
43
- <div className="space-y-3 flex-1">
62
+ {/* Phases with features */}
63
+ <div className="space-y-4">
44
64
  {roadmap.phases
45
65
  .filter(p => (p.features || []).length > 0)
46
- .slice(0, 4)
66
+ .slice(0, 6)
47
67
  .map((phase) => (
48
- <div key={phase.name}>
49
- <div className="flex items-center justify-between text-xs mb-1">
50
- <span className="font-medium truncate">{phase.name}</span>
51
- <span className="text-muted-foreground tabular-nums shrink-0 ml-2">
68
+ <div key={phase.name} className="group">
69
+ <div className="flex items-center justify-between mb-2">
70
+ <div className="flex items-center gap-2">
71
+ {phase.progress === 100 ? (
72
+ <CheckCircle2 className="h-4 w-4 text-muted-foreground" />
73
+ ) : (
74
+ <Circle className="h-4 w-4 text-muted-foreground" />
75
+ )}
76
+ <span className="text-sm font-medium">
77
+ {phase.name}
78
+ </span>
79
+ </div>
80
+ <span className="text-xs font-bold tabular-nums shrink-0 ml-2 text-muted-foreground">
52
81
  {phase.progress}%
53
82
  </span>
54
83
  </div>
55
- <div className="h-1.5 bg-muted rounded-full overflow-hidden">
84
+ <div className="h-2 bg-muted rounded-full overflow-hidden ml-6">
56
85
  <div
57
- className={cn(
58
- 'h-full rounded-full transition-all duration-300',
59
- phase.progress === 100 ? 'bg-emerald-500' : 'bg-foreground/70'
60
- )}
86
+ className={cn("h-full rounded-full transition-all duration-300", getProgressColor(phase.progress))}
61
87
  style={{ width: `${phase.progress}%` }}
62
88
  />
63
89
  </div>
90
+ {/* Show features under each phase */}
91
+ {phase.features && phase.features.length > 0 && (
92
+ <div className="ml-6 mt-2 space-y-1">
93
+ {phase.features.slice(0, 3).map((feature, i) => {
94
+ const isCompleted = feature.status === 'completed'
95
+ const cmdHref = codeHref && !isCompleted
96
+ ? `${codeHref}?cmd=${encodeURIComponent(`p. work "${feature.name}"`)}`
97
+ : undefined
98
+
99
+ const content = (
100
+ <>
101
+ <ChevronRight className="h-3 w-3 shrink-0" />
102
+ <span className={cn(
103
+ 'truncate',
104
+ isCompleted && 'line-through opacity-60'
105
+ )}>
106
+ {feature.name}
107
+ </span>
108
+ </>
109
+ )
110
+
111
+ return cmdHref ? (
112
+ <Link
113
+ key={i}
114
+ href={cmdHref}
115
+ className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/50 rounded px-1 -mx-1 py-0.5 transition-colors cursor-pointer"
116
+ >
117
+ {content}
118
+ </Link>
119
+ ) : (
120
+ <div key={i} className="flex items-center gap-2 text-xs text-muted-foreground">
121
+ {content}
122
+ </div>
123
+ )
124
+ })}
125
+ {phase.features.length > 3 && (
126
+ <span className="text-xs text-muted-foreground/70 ml-5">
127
+ +{phase.features.length - 3} more
128
+ </span>
129
+ )}
130
+ </div>
131
+ )}
64
132
  </div>
65
133
  ))}
66
134
  </div>
67
135
 
68
136
  {roadmap.phases.length > 0 && (
69
- <p className="text-[10px] text-muted-foreground mt-3">
137
+ <p className="text-xs text-muted-foreground border-t pt-3 mt-3">
70
138
  {roadmap.phases.reduce((acc, p) => acc + (p.features?.length || 0), 0)} features across {roadmap.phases.length} phases
71
139
  </p>
72
140
  )}
73
141
  </div>
74
142
  )}
75
- </BentoCard>
143
+ </div>
76
144
  )
77
145
  }
@@ -11,5 +11,6 @@ export interface RoadmapData {
11
11
 
12
12
  export interface RoadmapCardProps {
13
13
  roadmap: RoadmapData | null
14
+ codeHref?: string
14
15
  className?: string
15
16
  }
@@ -1,52 +1,95 @@
1
- import { BentoCard } from '@/components/BentoCard'
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
2
4
  import { EmptyState } from '@/components/EmptyState'
3
- import { Rocket } from 'lucide-react'
5
+ import { ExpandButton } from '@/components/ExpandButton'
6
+ import { Rocket, Clock, FileCode, Check } from 'lucide-react'
4
7
  import { Badge } from '@/components/ui/badge'
8
+ import { cn } from '@/lib/utils'
5
9
  import { formatShipDate } from './ShipsCard.utils'
6
10
  import type { ShipsCardProps } from './ShipsCard.types'
7
11
 
8
- export function ShipsCard({ ships, totalShips = 0, className }: ShipsCardProps) {
9
- const displayShips = ships.slice(0, 4)
12
+ const COLLAPSED_LIMIT = 10
13
+
14
+ export function ShipsCard({ ships, totalShips = 0, codeHref, className }: ShipsCardProps) {
15
+ const [expanded, setExpanded] = useState(false)
16
+ const displayShips = expanded ? ships : ships.slice(0, COLLAPSED_LIMIT)
17
+ const hasMore = ships.length > COLLAPSED_LIMIT
10
18
 
11
19
  return (
12
- <BentoCard
13
- size="1x2"
14
- title="Ships"
15
- icon={Rocket}
16
- count={totalShips}
17
- accentColor={ships.length > 0 ? 'success' : 'default'}
18
- className={className}
19
- >
20
+ <div className={cn(
21
+ 'relative overflow-hidden rounded-xl border bg-card p-4',
22
+ className
23
+ )}>
24
+ <div className="flex items-center justify-between mb-3">
25
+ <div className="flex items-center gap-2">
26
+ <Rocket className="h-4 w-4 text-muted-foreground" />
27
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
28
+ Shipped
29
+ </span>
30
+ </div>
31
+ <span className="text-xs font-medium text-muted-foreground tabular-nums">
32
+ {totalShips} total
33
+ </span>
34
+ </div>
35
+
20
36
  {ships.length === 0 ? (
21
37
  <EmptyState
22
38
  icon={Rocket}
23
39
  title="Nothing shipped yet"
24
40
  description="Ship your first feature"
25
41
  command="/p:ship"
42
+ href={codeHref}
26
43
  compact
27
44
  />
28
45
  ) : (
29
- <div className="space-y-3">
46
+ <div className="space-y-2">
30
47
  {displayShips.map((ship, i) => (
31
- <div key={i} className="group">
32
- <div className="flex items-center gap-2">
33
- {ship.version && (
34
- <Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono shrink-0">
35
- {ship.version}
36
- </Badge>
37
- )}
38
- <p className="text-sm font-medium truncate group-hover:text-foreground transition-colors">
39
- {ship.name}
40
- </p>
48
+ <div key={i} className="group py-1.5 hover:bg-muted/50 rounded px-2 -mx-2 border-l-2 border-transparent hover:border-foreground/20">
49
+ <div className="flex items-start justify-between gap-2">
50
+ <div className="flex-1 min-w-0">
51
+ <div className="flex items-center gap-2">
52
+ <Check className="h-3 w-3 text-muted-foreground shrink-0" />
53
+ <p className="text-sm font-medium truncate">
54
+ {ship.name}
55
+ </p>
56
+ {ship.version && (
57
+ <Badge variant="outline" className="text-xs px-1 py-0 font-mono shrink-0 h-4">
58
+ {ship.version}
59
+ </Badge>
60
+ )}
61
+ </div>
62
+ <div className="flex items-center gap-3 mt-1 ml-5">
63
+ <span className="text-xs text-muted-foreground">
64
+ {formatShipDate(ship.date)}
65
+ </span>
66
+ {ship.duration && (
67
+ <span className="inline-flex items-center gap-0.5 text-xs text-muted-foreground">
68
+ <Clock className="h-2.5 w-2.5" />
69
+ {ship.duration}
70
+ </span>
71
+ )}
72
+ {ship.filesChanged && (
73
+ <span className="inline-flex items-center gap-0.5 text-xs text-muted-foreground">
74
+ <FileCode className="h-2.5 w-2.5" />
75
+ {ship.filesChanged} files
76
+ </span>
77
+ )}
78
+ </div>
79
+ </div>
41
80
  </div>
42
- <p className="text-[10px] text-muted-foreground mt-0.5">
43
- {formatShipDate(ship.date)}
44
- {ship.duration && ` · ${ship.duration}`}
45
- </p>
46
81
  </div>
47
82
  ))}
83
+ {hasMore && (
84
+ <ExpandButton
85
+ expanded={expanded}
86
+ totalCount={ships.length}
87
+ collapsedLimit={COLLAPSED_LIMIT}
88
+ onToggle={() => setExpanded(!expanded)}
89
+ />
90
+ )}
48
91
  </div>
49
92
  )}
50
- </BentoCard>
93
+ </div>
51
94
  )
52
95
  }
@@ -3,10 +3,12 @@ export interface Ship {
3
3
  date: string
4
4
  version?: string
5
5
  duration?: string
6
+ filesChanged?: number
6
7
  }
7
8
 
8
9
  export interface ShipsCardProps {
9
10
  ships: Ship[]
10
11
  totalShips?: number
12
+ codeHref?: string
11
13
  className?: string
12
14
  }
@@ -16,23 +16,25 @@ export function SparklineChart({
16
16
  }
17
17
 
18
18
  return (
19
- <ResponsiveContainer width="100%" height={height}>
20
- <AreaChart data={chartData} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
21
- <defs>
22
- <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
23
- <stop offset="0%" stopColor={color} stopOpacity={0.3} />
24
- <stop offset="100%" stopColor={color} stopOpacity={0} />
25
- </linearGradient>
26
- </defs>
27
- <Area
28
- type="monotone"
29
- dataKey="value"
30
- stroke={color}
31
- strokeWidth={1.5}
32
- fill={showArea ? 'url(#sparklineGradient)' : 'none'}
33
- isAnimationActive={false}
34
- />
35
- </AreaChart>
36
- </ResponsiveContainer>
19
+ <div className="w-full min-w-0" style={{ height }}>
20
+ <ResponsiveContainer width="100%" height="100%">
21
+ <AreaChart data={chartData} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
22
+ <defs>
23
+ <linearGradient id="sparklineGradient" x1="0" y1="0" x2="0" y2="1">
24
+ <stop offset="0%" stopColor={color} stopOpacity={0.3} />
25
+ <stop offset="100%" stopColor={color} stopOpacity={0} />
26
+ </linearGradient>
27
+ </defs>
28
+ <Area
29
+ type="monotone"
30
+ dataKey="value"
31
+ stroke={color}
32
+ strokeWidth={1.5}
33
+ fill={showArea ? 'url(#sparklineGradient)' : 'none'}
34
+ isAnimationActive={false}
35
+ />
36
+ </AreaChart>
37
+ </ResponsiveContainer>
38
+ </div>
37
39
  )
38
40
  }