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
@@ -0,0 +1,187 @@
1
+ 'use client'
2
+
3
+ import { Rocket, CheckCircle2, Bug, Activity } from 'lucide-react'
4
+ import { cn } from '@/lib/utils'
5
+ import { formatDateRange } from '@/lib/generate-week-report'
6
+ import type { WeekData } from '@/lib/generate-week-report'
7
+
8
+ interface ReportPreviewCardProps {
9
+ weekData: WeekData[]
10
+ className?: string
11
+ }
12
+
13
+ export function ReportPreviewCard({ weekData, className }: ReportPreviewCardProps) {
14
+ if (weekData.length === 0) {
15
+ return (
16
+ <div className={cn('rounded-xl border bg-card p-6', className)}>
17
+ <p className="text-muted-foreground text-center">Select a week to preview</p>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ // Aggregate data from all selected weeks
23
+ const allShipped = weekData.flatMap(w => w.shipped)
24
+ const uniqueShipsMap = new Map<string, typeof allShipped[0]>()
25
+ for (const ship of allShipped) {
26
+ if (!uniqueShipsMap.has(ship.name)) {
27
+ uniqueShipsMap.set(ship.name, ship)
28
+ }
29
+ }
30
+ // Sort by date descending (most recent first)
31
+ const uniqueShips = Array.from(uniqueShipsMap.values())
32
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
33
+
34
+ // Group ships by date for display
35
+ const shipsByDate = new Map<string, typeof uniqueShips>()
36
+ for (const ship of uniqueShips) {
37
+ const dateKey = ship.date
38
+ if (!shipsByDate.has(dateKey)) {
39
+ shipsByDate.set(dateKey, [])
40
+ }
41
+ shipsByDate.get(dateKey)!.push(ship)
42
+ }
43
+
44
+ const totalTasks = weekData.reduce((sum, w) => sum + w.tasksCompleted, 0)
45
+ const totalBugs = weekData.reduce((sum, w) => sum + w.bugsFixed, 0)
46
+ const totalSyncs = weekData.reduce((sum, w) => sum + w.syncs, 0)
47
+ const totalDays = weekData.reduce((sum, w) => sum + w.activeDays, 0)
48
+
49
+ // Date range header
50
+ const firstWeek = weekData[0]
51
+ const lastWeek = weekData[weekData.length - 1]
52
+ const dateRangeStr = weekData.length === 1
53
+ ? `Week ${firstWeek.week} · ${formatDateRange(firstWeek.startDate, firstWeek.endDate)}`
54
+ : `Weeks ${firstWeek.week}-${lastWeek.week} · ${formatDateRange(firstWeek.startDate, lastWeek.endDate)}`
55
+
56
+ const hasNoData = uniqueShips.length === 0 && totalTasks === 0 && totalBugs === 0 && totalDays === 0
57
+
58
+ return (
59
+ <div className={cn('rounded-xl border bg-card overflow-hidden', className)}>
60
+ {/* Header */}
61
+ <div className="px-6 py-4 border-b bg-muted/30">
62
+ <h3 className="font-semibold text-lg">{dateRangeStr}</h3>
63
+ </div>
64
+
65
+ {hasNoData ? (
66
+ <div className="p-6">
67
+ <p className="text-muted-foreground text-center">No activity this week</p>
68
+ </div>
69
+ ) : (
70
+ <div className="p-6 space-y-6">
71
+ {/* Stats row */}
72
+ <div className="grid grid-cols-4 gap-4">
73
+ <StatCard
74
+ icon={Rocket}
75
+ value={uniqueShips.length}
76
+ label="shipped"
77
+ color="text-foreground"
78
+ bgColor="bg-muted"
79
+ />
80
+ <StatCard
81
+ icon={CheckCircle2}
82
+ value={totalTasks}
83
+ label="tasks"
84
+ color="text-foreground"
85
+ bgColor="bg-muted"
86
+ />
87
+ <StatCard
88
+ icon={Bug}
89
+ value={totalBugs}
90
+ label="bugs"
91
+ color="text-foreground"
92
+ bgColor="bg-muted"
93
+ />
94
+ <StatCard
95
+ icon={Activity}
96
+ value={totalDays}
97
+ label="days"
98
+ color="text-foreground"
99
+ bgColor="bg-muted"
100
+ />
101
+ </div>
102
+
103
+ {/* Shipped list grouped by date */}
104
+ {uniqueShips.length > 0 && (
105
+ <div>
106
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
107
+ Shipped
108
+ </h4>
109
+ <div className="space-y-4">
110
+ {Array.from(shipsByDate.entries()).map(([date, ships]) => (
111
+ <div key={date}>
112
+ <p className="text-xs text-muted-foreground mb-2">
113
+ {new Date(date).toLocaleDateString('es-MX', {
114
+ weekday: 'short',
115
+ month: 'short',
116
+ day: 'numeric'
117
+ })}
118
+ </p>
119
+ <ul className="space-y-1.5 pl-2 border-l-2 border-muted-foreground/30">
120
+ {ships.map((ship, i) => (
121
+ <li key={i} className="flex items-start gap-2 pl-2">
122
+ <span className="flex-1">
123
+ {ship.name}
124
+ {ship.version && (
125
+ <span className="text-muted-foreground ml-1 text-sm">({ship.version})</span>
126
+ )}
127
+ </span>
128
+ </li>
129
+ ))}
130
+ </ul>
131
+ </div>
132
+ ))}
133
+ </div>
134
+ </div>
135
+ )}
136
+
137
+ {/* Activity summary */}
138
+ {(totalTasks > 0 || totalBugs > 0 || totalSyncs > 0) && (
139
+ <div>
140
+ <h4 className="text-xs font-medium text-muted-foreground uppercase tracking-wider mb-3">
141
+ Activity
142
+ </h4>
143
+ <ul className="space-y-1 text-sm text-muted-foreground">
144
+ {totalTasks > 0 && (
145
+ <li className="flex items-center gap-2">
146
+ <CheckCircle2 className="h-3.5 w-3.5 text-muted-foreground" />
147
+ {totalTasks} task{totalTasks !== 1 ? 's' : ''} completed
148
+ </li>
149
+ )}
150
+ {totalBugs > 0 && (
151
+ <li className="flex items-center gap-2">
152
+ <Bug className="h-3.5 w-3.5 text-muted-foreground" />
153
+ {totalBugs} bug{totalBugs !== 1 ? 's' : ''} fixed
154
+ </li>
155
+ )}
156
+ {totalSyncs > 0 && (
157
+ <li className="flex items-center gap-2">
158
+ <Activity className="h-3.5 w-3.5 text-muted-foreground" />
159
+ {totalSyncs} sync{totalSyncs !== 1 ? 's' : ''}
160
+ </li>
161
+ )}
162
+ </ul>
163
+ </div>
164
+ )}
165
+ </div>
166
+ )}
167
+ </div>
168
+ )
169
+ }
170
+
171
+ interface StatCardProps {
172
+ icon: React.ElementType
173
+ value: number
174
+ label: string
175
+ color: string
176
+ bgColor: string
177
+ }
178
+
179
+ function StatCard({ icon: Icon, value, label, color, bgColor }: StatCardProps) {
180
+ return (
181
+ <div className={cn('rounded-xl p-4 text-center', bgColor)}>
182
+ <Icon className={cn('h-5 w-5 mx-auto mb-1', color)} />
183
+ <div className="text-2xl font-bold tabular-nums">{value}</div>
184
+ <div className="text-xs text-muted-foreground">{label}</div>
185
+ </div>
186
+ )
187
+ }
@@ -0,0 +1,288 @@
1
+ 'use client'
2
+
3
+ import { useRef, useEffect, useState } from 'react'
4
+ import { ChevronLeft, ChevronRight } from 'lucide-react'
5
+ import { cn } from '@/lib/utils'
6
+ import { getCurrentYearWeek, getWeekDateRange, formatDateRange } from '@/lib/generate-week-report'
7
+
8
+ export type ActivityLevel = 'none' | 'low' | 'medium' | 'high'
9
+
10
+ interface WeekCalendarProps {
11
+ year: number
12
+ selectedWeeks: number[]
13
+ activityLevels: Map<number, ActivityLevel>
14
+ onWeekSelect: (weeks: number[]) => void
15
+ onYearChange: (year: number) => void
16
+ }
17
+
18
+ const DAYS = ['M', 'T', 'W', 'T', 'F'] // Mon-Fri
19
+
20
+ export function WeekCalendar({
21
+ year,
22
+ selectedWeeks,
23
+ activityLevels,
24
+ onWeekSelect,
25
+ onYearChange,
26
+ }: WeekCalendarProps) {
27
+ const { year: currentYear, week: currentWeek } = getCurrentYearWeek()
28
+ const isCurrentYear = year === currentYear
29
+ const scrollRef = useRef<HTMLDivElement>(null)
30
+ const [canScrollLeft, setCanScrollLeft] = useState(false)
31
+ const [canScrollRight, setCanScrollRight] = useState(true)
32
+
33
+ // Generate 52 weeks
34
+ const weeks = Array.from({ length: 52 }, (_, i) => i + 1)
35
+
36
+ // Drag to scroll state
37
+ const [isDragging, setIsDragging] = useState(false)
38
+ const [startX, setStartX] = useState(0)
39
+ const [scrollLeftStart, setScrollLeftStart] = useState(0)
40
+ const [hasMoved, setHasMoved] = useState(false)
41
+
42
+ // Handle mouse down - start potential drag
43
+ const handleMouseDown = (e: React.MouseEvent) => {
44
+ if (!scrollRef.current) return
45
+ setIsDragging(true)
46
+ setStartX(e.pageX)
47
+ setScrollLeftStart(scrollRef.current.scrollLeft)
48
+ setHasMoved(false)
49
+ }
50
+
51
+ // Handle mouse move - scroll if dragging
52
+ const handleMouseMove = (e: React.MouseEvent) => {
53
+ if (!isDragging || !scrollRef.current) return
54
+
55
+ const diff = e.pageX - startX
56
+ // Only count as drag if moved more than 5px
57
+ if (Math.abs(diff) > 5) {
58
+ setHasMoved(true)
59
+ e.preventDefault()
60
+ scrollRef.current.scrollLeft = scrollLeftStart - diff
61
+ }
62
+ }
63
+
64
+ // Handle mouse up - end drag
65
+ const handleMouseUp = () => {
66
+ setIsDragging(false)
67
+ }
68
+
69
+ // Handle week click (only if not dragging)
70
+ const handleWeekClick = (week: number, e: React.MouseEvent) => {
71
+ // Ignore click if we were dragging
72
+ if (hasMoved) {
73
+ e.preventDefault()
74
+ return
75
+ }
76
+
77
+ const isFuture = isCurrentYear && week > currentWeek
78
+ if (isFuture) return
79
+
80
+ if (selectedWeeks.includes(week)) {
81
+ onWeekSelect(selectedWeeks.filter(w => w !== week))
82
+ } else {
83
+ onWeekSelect([...selectedWeeks, week].sort((a, b) => a - b))
84
+ }
85
+ }
86
+
87
+ // Check scroll state
88
+ const updateScrollState = () => {
89
+ if (scrollRef.current) {
90
+ const { scrollLeft, scrollWidth, clientWidth } = scrollRef.current
91
+ setCanScrollLeft(scrollLeft > 0)
92
+ setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 10)
93
+ }
94
+ }
95
+
96
+ useEffect(() => {
97
+ updateScrollState()
98
+ // Scroll to current week on mount
99
+ if (scrollRef.current && isCurrentYear) {
100
+ const weekElement = scrollRef.current.querySelector(`[data-week="${currentWeek}"]`)
101
+ if (weekElement) {
102
+ weekElement.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' })
103
+ }
104
+ }
105
+ }, [currentWeek, isCurrentYear])
106
+
107
+ const scroll = (direction: 'left' | 'right') => {
108
+ if (scrollRef.current) {
109
+ const scrollAmount = 400
110
+ scrollRef.current.scrollBy({
111
+ left: direction === 'left' ? -scrollAmount : scrollAmount,
112
+ behavior: 'smooth'
113
+ })
114
+ }
115
+ }
116
+
117
+ return (
118
+ <div className="space-y-6">
119
+ {/* Year header */}
120
+ <div className="flex items-center justify-between">
121
+ <div className="flex items-center gap-2">
122
+ <button
123
+ onClick={() => onYearChange(year - 1)}
124
+ className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
125
+ >
126
+ <ChevronLeft className="h-4 w-4" />
127
+ </button>
128
+
129
+ <span className="text-4xl font-bold tabular-nums">{year}</span>
130
+
131
+ <button
132
+ onClick={() => onYearChange(year + 1)}
133
+ disabled={year >= currentYear}
134
+ className="p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
135
+ >
136
+ <ChevronRight className="h-4 w-4" />
137
+ </button>
138
+ </div>
139
+
140
+ <span className="text-sm text-muted-foreground">
141
+ {selectedWeeks.length === 0
142
+ ? 'Select weeks'
143
+ : selectedWeeks.length === 1
144
+ ? `W${selectedWeeks[0]}`
145
+ : `${selectedWeeks.length} wks`
146
+ }
147
+ </span>
148
+ </div>
149
+
150
+ {/* Horizontal week strip */}
151
+ <div className="relative -mx-2">
152
+ {/* Scroll buttons */}
153
+ {canScrollLeft && (
154
+ <button
155
+ onClick={() => scroll('left')}
156
+ className="absolute left-0 top-1/2 -translate-y-1/2 z-10 p-2 bg-background/80 backdrop-blur rounded-full shadow-lg border hover:bg-muted transition-colors"
157
+ >
158
+ <ChevronLeft className="h-4 w-4" />
159
+ </button>
160
+ )}
161
+ {canScrollRight && (
162
+ <button
163
+ onClick={() => scroll('right')}
164
+ className="absolute right-0 top-1/2 -translate-y-1/2 z-10 p-2 bg-background/80 backdrop-blur rounded-full shadow-lg border hover:bg-muted transition-colors"
165
+ >
166
+ <ChevronRight className="h-4 w-4" />
167
+ </button>
168
+ )}
169
+
170
+ {/* Scrollable week cards - drag to scroll */}
171
+ <div
172
+ ref={scrollRef}
173
+ onScroll={updateScrollState}
174
+ onMouseDown={handleMouseDown}
175
+ onMouseMove={handleMouseMove}
176
+ onMouseUp={handleMouseUp}
177
+ onMouseLeave={handleMouseUp}
178
+ className={cn(
179
+ 'flex gap-3 overflow-x-auto scrollbar-hide py-2 px-2',
180
+ isDragging && hasMoved ? 'cursor-grabbing' : 'cursor-grab'
181
+ )}
182
+ style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
183
+ >
184
+ {weeks.map((week) => {
185
+ const isSelected = selectedWeeks.includes(week)
186
+ const isCurrent = isCurrentYear && week === currentWeek
187
+ const activityLevel = activityLevels.get(week) ?? 'none'
188
+ const isFuture = isCurrentYear && week > currentWeek
189
+ const { start, end } = getWeekDateRange(year, week)
190
+
191
+ // Get month name
192
+ const monthName = start.toLocaleDateString('en-US', { month: 'short' })
193
+ const dayStart = start.getDate()
194
+ const dayEnd = end.getDate()
195
+
196
+ return (
197
+ <div
198
+ key={week}
199
+ data-week={week}
200
+ onClick={(e) => handleWeekClick(week, e)}
201
+ className={cn(
202
+ 'flex-shrink-0 w-28 rounded-2xl transition-all duration-200 select-none',
203
+ 'flex flex-col overflow-hidden',
204
+ 'border',
205
+ isFuture && 'opacity-30',
206
+ // Selected state
207
+ isSelected
208
+ ? 'bg-foreground text-background border-foreground shadow-lg scale-105'
209
+ : 'bg-card hover:bg-muted border-border',
210
+ isCurrent && !isSelected && 'ring-2 ring-primary ring-offset-2 ring-offset-background',
211
+ )}
212
+ >
213
+ {/* Week header */}
214
+ <div className={cn(
215
+ 'px-3 py-2 text-left border-b',
216
+ isSelected ? 'border-background/20' : 'border-border'
217
+ )}>
218
+ <div className="flex items-baseline gap-0.5">
219
+ <span className={cn(
220
+ 'text-xs font-medium',
221
+ isSelected ? 'text-background/60' : 'text-muted-foreground'
222
+ )}>
223
+ W
224
+ </span>
225
+ <span className="text-2xl font-bold tabular-nums">{week}</span>
226
+ </div>
227
+ <p className={cn(
228
+ 'text-xs mt-0.5',
229
+ isSelected ? 'text-background/70' : 'text-muted-foreground'
230
+ )}>
231
+ {monthName} {dayStart}-{dayEnd}
232
+ </p>
233
+ </div>
234
+
235
+ {/* Days grid - Mon to Fri with GitHub-style activity levels */}
236
+ <div className="px-3 py-2">
237
+ <div className="flex justify-between gap-1">
238
+ {DAYS.map((day, i) => {
239
+ // Generate activity level per day based on week activity
240
+ // This simulates varying activity levels across the week
241
+ let dayActivityLevel: 0 | 1 | 2 | 3 | 4 = 0
242
+
243
+ if (activityLevel === 'high') {
244
+ // High activity: most days are 3-4
245
+ dayActivityLevel = [4, 3, 4, 3, 2][i] as 0 | 1 | 2 | 3 | 4
246
+ } else if (activityLevel === 'medium') {
247
+ // Medium: mix of 2-3
248
+ dayActivityLevel = [2, 3, 2, 1, 2][i] as 0 | 1 | 2 | 3 | 4
249
+ } else if (activityLevel === 'low') {
250
+ // Low: mostly 0-1
251
+ dayActivityLevel = [1, 0, 1, 0, 0][i] as 0 | 1 | 2 | 3 | 4
252
+ }
253
+
254
+ // GitHub-style green gradients
255
+ const activityColors = {
256
+ 0: isSelected ? 'bg-background/10' : 'bg-muted',
257
+ 1: isSelected ? 'bg-background/30' : 'bg-emerald-500/30',
258
+ 2: isSelected ? 'bg-background/50' : 'bg-emerald-500/50',
259
+ 3: isSelected ? 'bg-background/75' : 'bg-emerald-500/75',
260
+ 4: isSelected ? 'bg-background' : 'bg-emerald-500',
261
+ }
262
+
263
+ return (
264
+ <div key={i} className="flex flex-col items-center gap-1">
265
+ <span className={cn(
266
+ 'text-[10px] font-medium',
267
+ isSelected ? 'text-background/50' : 'text-muted-foreground'
268
+ )}>
269
+ {day}
270
+ </span>
271
+ <div className={cn(
272
+ 'w-3 h-3 rounded-sm',
273
+ activityColors[dayActivityLevel]
274
+ )} />
275
+ </div>
276
+ )
277
+ })}
278
+ </div>
279
+ </div>
280
+ </div>
281
+ )
282
+ })}
283
+ </div>
284
+ </div>
285
+
286
+ </div>
287
+ )
288
+ }
@@ -0,0 +1,149 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { RotateCcw, Copy, Check, Printer } from 'lucide-react'
7
+ import { WeekCalendar, type ActivityLevel } from './WeekCalendar'
8
+ import { ReportPreviewCard } from './ReportPreviewCard'
9
+ import {
10
+ getCurrentYearWeek,
11
+ filterDataByWeek,
12
+ generateReportText,
13
+ getWeekActivityLevel,
14
+ } from '@/lib/generate-week-report'
15
+ import type { StatsResult } from '@/lib/services/stats.server'
16
+ import { cn } from '@/lib/utils'
17
+
18
+ interface WeeklyReportsProps {
19
+ stats: StatsResult
20
+ projectName: string
21
+ }
22
+
23
+ export function WeeklyReports({ stats, projectName }: WeeklyReportsProps) {
24
+ const params = useParams()
25
+ const projectId = params.id as string
26
+ const { year: currentYear, week: currentWeek } = getCurrentYearWeek()
27
+
28
+ const [year, setYear] = useState(currentYear)
29
+ const [selectedWeeks, setSelectedWeeks] = useState<number[]>([currentWeek])
30
+ const [copied, setCopied] = useState(false)
31
+
32
+ // Build print URL with selected weeks
33
+ const printUrl = `/project/${projectId}/reports/print?weeks=${selectedWeeks.join(',')}&year=${year}`
34
+
35
+ const activityLevels = useMemo(() => {
36
+ const levels = new Map<number, ActivityLevel>()
37
+ for (let w = 1; w <= 52; w++) {
38
+ levels.set(w, getWeekActivityLevel(stats, year, w))
39
+ }
40
+ return levels
41
+ }, [stats, year])
42
+
43
+ const handleReset = () => {
44
+ setYear(currentYear)
45
+ setSelectedWeeks([currentWeek])
46
+ }
47
+
48
+ const weekDataList = useMemo(() => {
49
+ return selectedWeeks.map(w => filterDataByWeek(stats, year, w))
50
+ }, [stats, year, selectedWeeks])
51
+
52
+ const reportText = useMemo(() => {
53
+ if (weekDataList.length === 0) return ''
54
+ return generateReportText(weekDataList, projectName)
55
+ }, [weekDataList, projectName])
56
+
57
+ const handleCopy = async () => {
58
+ try {
59
+ await navigator.clipboard.writeText(reportText)
60
+ setCopied(true)
61
+ setTimeout(() => setCopied(false), 2000)
62
+ } catch (err) {
63
+ console.error('Failed to copy:', err)
64
+ }
65
+ }
66
+
67
+ return (
68
+ <div className="space-y-6">
69
+ {/* Calendar */}
70
+ <div className="relative overflow-hidden rounded-xl border bg-card p-6">
71
+ <WeekCalendar
72
+ year={year}
73
+ selectedWeeks={selectedWeeks}
74
+ activityLevels={activityLevels}
75
+ onWeekSelect={setSelectedWeeks}
76
+ onYearChange={setYear}
77
+ />
78
+
79
+ {/* Actions */}
80
+ <div className="flex items-center justify-between mt-6 pt-4 border-t">
81
+ <button
82
+ onClick={handleReset}
83
+ className="inline-flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-muted rounded-lg transition-colors"
84
+ >
85
+ <RotateCcw className="h-4 w-4" />
86
+ Reset
87
+ </button>
88
+
89
+ <div className="flex items-center gap-2">
90
+ <Link
91
+ href={printUrl}
92
+ target="_blank"
93
+ className={cn(
94
+ 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all',
95
+ 'border border-foreground/20 text-foreground hover:bg-muted',
96
+ selectedWeeks.length === 0 && 'opacity-50 pointer-events-none'
97
+ )}
98
+ >
99
+ <Printer className="h-4 w-4" />
100
+ Print / PDF
101
+ </Link>
102
+
103
+ <button
104
+ onClick={handleCopy}
105
+ disabled={selectedWeeks.length === 0}
106
+ className={cn(
107
+ 'inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all',
108
+ copied
109
+ ? 'bg-emerald-500 text-white'
110
+ : 'bg-foreground text-background hover:bg-foreground/90',
111
+ selectedWeeks.length === 0 && 'opacity-50 cursor-not-allowed'
112
+ )}
113
+ >
114
+ {copied ? (
115
+ <>
116
+ <Check className="h-4 w-4" />
117
+ Copied!
118
+ </>
119
+ ) : (
120
+ <>
121
+ <Copy className="h-4 w-4" />
122
+ Copy
123
+ </>
124
+ )}
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </div>
129
+
130
+ {/* Report Preview Card */}
131
+ <ReportPreviewCard weekData={weekDataList} />
132
+
133
+ {/* Copy Text Preview (collapsed) */}
134
+ {selectedWeeks.length > 0 && reportText && (
135
+ <details className="group">
136
+ <summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground flex items-center gap-2">
137
+ <span className="group-open:rotate-90 transition-transform">▶</span>
138
+ View copy text
139
+ </summary>
140
+ <div className="mt-3 relative overflow-hidden rounded-xl border bg-muted/30 p-4">
141
+ <pre className="whitespace-pre-wrap text-sm font-mono text-muted-foreground">
142
+ {reportText}
143
+ </pre>
144
+ </div>
145
+ </details>
146
+ )}
147
+ </div>
148
+ )
149
+ }
@@ -0,0 +1,4 @@
1
+ export { WeeklyReports } from './WeeklyReports'
2
+ export { WeekCalendar } from './WeekCalendar'
3
+ export { ReportPreviewCard } from './ReportPreviewCard'
4
+ export { PrintableReport } from './PrintableReport'
@@ -1,13 +1,25 @@
1
+ 'use client'
2
+
1
3
  import { SparklineChart } from '@/components/SparklineChart'
2
4
  import type { WeeklySparklineProps } from './WeeklySparkline.types'
3
5
 
4
- export function WeeklySparkline({ data }: WeeklySparklineProps) {
6
+ const sizeConfig = {
7
+ sm: { width: 'w-full', height: 32 },
8
+ md: { width: 'w-full', height: 40 },
9
+ lg: { width: 'w-full', height: 56 },
10
+ }
11
+
12
+ export function WeeklySparkline({ data, size = 'md' }: WeeklySparklineProps) {
13
+ const config = sizeConfig[size]
14
+
5
15
  return (
6
- <div className="w-full sm:w-32">
7
- <p className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1 md:text-right">
16
+ <div className={`${config.width} min-w-0`}>
17
+ <p className="text-xs uppercase tracking-wider text-muted-foreground mb-1 md:text-right">
8
18
  7-day activity
9
19
  </p>
10
- <SparklineChart data={data} height={40} />
20
+ <div className="w-full min-w-0 overflow-hidden">
21
+ <SparklineChart data={data} height={config.height} />
22
+ </div>
11
23
  </div>
12
24
  )
13
25
  }
@@ -1,3 +1,4 @@
1
1
  export interface WeeklySparklineProps {
2
2
  data: number[]
3
+ size?: 'sm' | 'md' | 'lg'
3
4
  }