@prmichaelsen/acp-visualizer 0.9.1 → 0.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -14,15 +14,18 @@ export function Header({ data }: HeaderProps) {
14
14
  if (!data) return null
15
15
 
16
16
  return (
17
- <header className="h-14 border-b border-gray-200 dark:border-gray-800 flex items-center px-6 gap-4 shrink-0 bg-white dark:bg-gray-950">
18
- <h1 className="text-sm font-medium text-gray-900 dark:text-gray-200">{data.project.name}</h1>
19
- <span className="text-xs text-gray-500 dark:text-gray-500 font-mono">v{data.project.version}</span>
20
- <StatusBadge status={data.project.status} />
21
- <div className="ml-auto flex items-center gap-4">
22
- <div className="flex items-center gap-3 w-48">
17
+ <header className="h-14 border-b border-gray-200 dark:border-gray-800 flex items-center pl-16 lg:pl-6 pr-4 lg:pr-6 gap-2 lg:gap-4 shrink-0 bg-white dark:bg-gray-950">
18
+ <h1 className="text-sm font-medium text-gray-900 dark:text-gray-200 truncate">{data.project.name}</h1>
19
+ <span className="hidden sm:inline text-xs text-gray-500 dark:text-gray-500 font-mono">v{data.project.version}</span>
20
+ <div className="hidden sm:block">
21
+ <StatusBadge status={data.project.status} />
22
+ </div>
23
+ <div className="ml-auto flex items-center gap-2 lg:gap-4">
24
+ <div className="hidden md:flex items-center gap-3 w-32 lg:w-48">
23
25
  <ProgressBar value={data.progress.overall} size="sm" />
24
26
  <span className="text-xs text-gray-600 dark:text-gray-400 font-mono">{data.progress.overall}%</span>
25
27
  </div>
28
+ <span className="md:hidden text-xs text-gray-600 dark:text-gray-400 font-mono">{data.progress.overall}%</span>
26
29
  <button
27
30
  onClick={toggleTheme}
28
31
  className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
@@ -1,7 +1,8 @@
1
1
  import { Link } from '@tanstack/react-router'
2
2
  import { useState, useEffect, useMemo } from 'react'
3
- import { ExternalLink } from 'lucide-react'
3
+ import { Maximize2 } from 'lucide-react'
4
4
  import { useProgressData } from '../contexts/ProgressContext'
5
+ import { useSidePanel } from '../contexts/SidePanelContext'
5
6
  import { DetailHeader } from './DetailHeader'
6
7
  import { ProgressBar } from './ProgressBar'
7
8
  import { StatusDot } from './StatusDot'
@@ -27,6 +28,7 @@ function getGitHubParams(): { owner: string; repo: string } | undefined {
27
28
 
28
29
  export function MilestonePreview({ milestoneId }: MilestonePreviewProps) {
29
30
  const data = useProgressData()
31
+ const { close } = useSidePanel()
30
32
  const [markdown, setMarkdown] = useState<string | null>(null)
31
33
  const [markdownError, setMarkdownError] = useState<string | null>(null)
32
34
  const [markdownFilePath, setMarkdownFilePath] = useState<string | null>(null)
@@ -96,8 +98,9 @@ export function MilestonePreview({ milestoneId }: MilestonePreviewProps) {
96
98
  params={{ milestoneId }}
97
99
  className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
98
100
  title="Open full view"
101
+ onClick={close}
99
102
  >
100
- <ExternalLink className="w-4 h-4 text-gray-600 dark:text-gray-400" />
103
+ <Maximize2 className="w-4 h-4 text-gray-600 dark:text-gray-400" />
101
104
  </Link>
102
105
  </div>
103
106
 
@@ -36,6 +36,63 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
36
36
  })
37
37
  }
38
38
 
39
+ // Mobile card view
40
+ const MobileCard = ({ milestone }: { milestone: Milestone }) => {
41
+ const milestoneTasks = tasks[milestone.id] || []
42
+ const isExpanded = expanded.has(milestone.id)
43
+
44
+ return (
45
+ <div className="border border-gray-200 dark:border-gray-800 rounded-lg p-4 space-y-3">
46
+ <div className="flex items-start justify-between gap-2">
47
+ <div className="flex-1 min-w-0">
48
+ <Link
49
+ to="/milestones/$milestoneId"
50
+ params={{ milestoneId: milestone.id }}
51
+ className="text-sm font-medium text-gray-900 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400"
52
+ >
53
+ {formatMilestoneName(milestone)}
54
+ </Link>
55
+ </div>
56
+ <PreviewButton type="milestone" id={milestone.id} />
57
+ </div>
58
+
59
+ <div className="flex items-center gap-2">
60
+ <StatusBadge status={milestone.status} />
61
+ <PriorityBadge priority={milestone.priority} />
62
+ </div>
63
+
64
+ <div className="flex items-center gap-2">
65
+ <div className="flex-1">
66
+ <ProgressBar value={milestone.progress} size="sm" />
67
+ </div>
68
+ <span className="text-xs text-gray-600 dark:text-gray-500 font-mono">{milestone.progress}%</span>
69
+ </div>
70
+
71
+ <div className="flex items-center justify-between text-xs text-gray-600 dark:text-gray-500">
72
+ <span>Tasks: {milestone.tasks_completed}/{milestone.tasks_total}</span>
73
+ {milestoneTasks.length > 0 && (
74
+ <button
75
+ onClick={() => toggle(milestone.id)}
76
+ className="flex items-center gap-1 hover:text-gray-900 dark:hover:text-gray-300"
77
+ >
78
+ {isExpanded ? (
79
+ <ChevronDown className="w-4 h-4" />
80
+ ) : (
81
+ <ChevronRight className="w-4 h-4" />
82
+ )}
83
+ </button>
84
+ )}
85
+ </div>
86
+
87
+ {isExpanded && milestoneTasks.length > 0 && (
88
+ <div className="pt-2 border-t border-gray-200 dark:border-gray-800">
89
+ <TaskList tasks={milestoneTasks} />
90
+ </div>
91
+ )}
92
+ </div>
93
+ )
94
+ }
95
+
39
96
  const columns = [
40
97
  columnHelper.display({
41
98
  id: 'expand',
@@ -135,8 +192,17 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
135
192
  }
136
193
 
137
194
  return (
138
- <div className="border border-gray-800 rounded-lg overflow-hidden">
139
- <table className="w-full">
195
+ <>
196
+ {/* Mobile Card View */}
197
+ <div className="lg:hidden space-y-3">
198
+ {milestones.map((milestone) => (
199
+ <MobileCard key={milestone.id} milestone={milestone} />
200
+ ))}
201
+ </div>
202
+
203
+ {/* Desktop Table View */}
204
+ <div className="hidden lg:block border border-gray-800 rounded-lg overflow-hidden">
205
+ <table className="w-full">
140
206
  <thead>
141
207
  {table.getHeaderGroups().map((headerGroup) => (
142
208
  <tr key={headerGroup.id} className="border-b border-gray-800 bg-gray-900/50">
@@ -181,6 +247,7 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
181
247
  ))}
182
248
  </tbody>
183
249
  </table>
184
- </div>
250
+ </div>
251
+ </>
185
252
  )
186
253
  }
@@ -23,7 +23,7 @@ export function PreviewButton({ type, id, className = '' }: PreviewButtonProps)
23
23
  return (
24
24
  <button
25
25
  onClick={handleClick}
26
- className={`p-1.5 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors opacity-0 group-hover:opacity-100 ${className}`}
26
+ className={`p-1.5 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors ${className}`}
27
27
  title={`Preview ${type}`}
28
28
  aria-label={`Preview ${type}`}
29
29
  >
@@ -23,8 +23,8 @@ export function SidePanel() {
23
23
 
24
24
  {/* Panel */}
25
25
  <div
26
- className={`fixed top-0 right-0 h-full w-full max-w-2xl bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-800 shadow-2xl z-50 transition-transform duration-300 overflow-auto translate-x-full ${
27
- isOpen ? '!translate-x-0' : ''
26
+ className={`fixed top-0 right-0 h-full w-full lg:max-w-2xl bg-white dark:bg-gray-900 border-l border-gray-200 dark:border-gray-800 shadow-2xl z-50 overflow-auto transition-transform duration-300 ${
27
+ isOpen ? 'translate-x-0' : 'translate-x-full'
28
28
  }`}
29
29
  >
30
30
  {/* Header */}
@@ -1,5 +1,5 @@
1
1
  import { Link, useRouterState } from '@tanstack/react-router'
2
- import { LayoutDashboard, Flag, CheckSquare, Clock, Search, PenTool, Puzzle, FileBarChart } from 'lucide-react'
2
+ import { LayoutDashboard, Flag, CheckSquare, Clock, Search, PenTool, Puzzle, FileBarChart, X } from 'lucide-react'
3
3
  import { ProjectSelector } from './ProjectSelector'
4
4
  import { GitHubInput } from './GitHubInput'
5
5
  import type { AcpProject } from '../services/projects.service'
@@ -19,17 +19,27 @@ interface SidebarProps {
19
19
  currentProject?: string | null
20
20
  onProjectSelect?: (projectId: string) => void
21
21
  onGitHubLoad?: (owner: string, repo: string) => Promise<void>
22
+ onClose?: () => void
22
23
  }
23
24
 
24
- export function Sidebar({ projects = [], currentProject = null, onProjectSelect, onGitHubLoad }: SidebarProps) {
25
+ export function Sidebar({ projects = [], currentProject = null, onProjectSelect, onGitHubLoad, onClose }: SidebarProps) {
25
26
  const location = useRouterState({ select: (s) => s.location })
26
27
 
27
28
  return (
28
- <nav className="w-56 border-r border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-950 flex flex-col shrink-0">
29
- <div className="p-4 border-b border-gray-200 dark:border-gray-800">
29
+ <nav className="w-56 h-full border-r border-gray-200 dark:border-gray-800 bg-gray-100 dark:bg-gray-950 flex flex-col shrink-0">
30
+ <div className="p-4 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between">
30
31
  <span className="text-sm font-semibold text-gray-700 dark:text-gray-300 tracking-wide">
31
32
  ACP Visualizer
32
33
  </span>
34
+ {onClose && (
35
+ <button
36
+ onClick={onClose}
37
+ className="lg:hidden p-1.5 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors"
38
+ aria-label="Close menu"
39
+ >
40
+ <X className="w-4 h-4 text-gray-700 dark:text-gray-300" />
41
+ </button>
42
+ )}
33
43
  </div>
34
44
  {projects.length > 1 && onProjectSelect && (
35
45
  <div className="px-3 pt-3">
@@ -1,7 +1,8 @@
1
1
  import { Link } from '@tanstack/react-router'
2
2
  import { useState, useEffect, useMemo } from 'react'
3
- import { ExternalLink } from 'lucide-react'
3
+ import { Maximize2 } from 'lucide-react'
4
4
  import { useProgressData } from '../contexts/ProgressContext'
5
+ import { useSidePanel } from '../contexts/SidePanelContext'
5
6
  import { DetailHeader } from './DetailHeader'
6
7
  import { PriorityBadge } from './PriorityBadge'
7
8
  import { MarkdownContent, buildLinkMap } from './MarkdownContent'
@@ -25,6 +26,7 @@ function getGitHubParams(): { owner: string; repo: string } | undefined {
25
26
 
26
27
  export function TaskPreview({ taskId }: TaskPreviewProps) {
27
28
  const data = useProgressData()
29
+ const { close } = useSidePanel()
28
30
  const [markdown, setMarkdown] = useState<string | null>(null)
29
31
  const [markdownError, setMarkdownError] = useState<string | null>(null)
30
32
  const [loading, setLoading] = useState(true)
@@ -116,8 +118,9 @@ export function TaskPreview({ taskId }: TaskPreviewProps) {
116
118
  params={{ taskId }}
117
119
  className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
118
120
  title="Open full view"
121
+ onClick={close}
119
122
  >
120
- <ExternalLink className="w-4 h-4 text-gray-600 dark:text-gray-400" />
123
+ <Maximize2 className="w-4 h-4 text-gray-600 dark:text-gray-400" />
121
124
  </Link>
122
125
  </div>
123
126
 
@@ -1,5 +1,6 @@
1
1
  import { HeadContent, Scripts, createRootRoute, Outlet, useRouter, useRouterState } from '@tanstack/react-router'
2
2
  import { useState, useCallback, useEffect } from 'react'
3
+ import { Menu, X } from 'lucide-react'
3
4
  import { useAutoRefresh } from '../lib/useAutoRefresh'
4
5
  import { Sidebar } from '../components/Sidebar'
5
6
  import { Header } from '../components/Header'
@@ -109,6 +110,7 @@ function RootLayout() {
109
110
  context.progressData?.project.name || null,
110
111
  )
111
112
  const [initialLoadDone, setInitialLoadDone] = useState(false)
113
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
112
114
 
113
115
  // On mount, check for ?repo= param and auto-load
114
116
  useEffect(() => {
@@ -157,21 +159,51 @@ function RootLayout() {
157
159
  <>
158
160
  <AutoRefresh />
159
161
  <div className="flex h-screen bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100">
160
- <Sidebar
161
- projects={context.projects}
162
- currentProject={currentProject}
163
- onProjectSelect={handleProjectSwitch}
164
- onGitHubLoad={handleGitHubLoad}
165
- />
162
+ {/* Mobile Menu Button */}
163
+ <button
164
+ onClick={() => setMobileMenuOpen(true)}
165
+ className="lg:hidden fixed top-4 left-4 z-50 p-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-lg"
166
+ aria-label="Open menu"
167
+ >
168
+ <Menu className="w-5 h-5 text-gray-900 dark:text-gray-100" />
169
+ </button>
170
+
171
+ {/* Mobile Backdrop */}
172
+ {mobileMenuOpen && (
173
+ <div
174
+ className="lg:hidden fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
175
+ onClick={() => setMobileMenuOpen(false)}
176
+ />
177
+ )}
178
+
179
+ {/* Sidebar - Desktop: Always visible | Mobile: Drawer */}
180
+ <div
181
+ className={`fixed lg:relative inset-y-0 left-0 z-50 transition-transform duration-300 lg:translate-x-0 ${
182
+ mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
183
+ }`}
184
+ >
185
+ <Sidebar
186
+ projects={context.projects}
187
+ currentProject={currentProject}
188
+ onProjectSelect={handleProjectSwitch}
189
+ onGitHubLoad={handleGitHubLoad}
190
+ onClose={() => setMobileMenuOpen(false)}
191
+ />
192
+ </div>
193
+
166
194
  <ProgressProvider data={progressData}>
167
195
  <SidePanelProvider>
168
- <div className="flex-1 flex flex-col overflow-hidden">
169
- <Header data={progressData} />
170
- <main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900">
171
- <Outlet />
172
- </main>
196
+ <div className="flex-1 flex overflow-hidden">
197
+ {/* Main content area */}
198
+ <div className="flex-1 flex flex-col overflow-hidden">
199
+ <Header data={progressData} />
200
+ <main className="flex-1 overflow-auto bg-gray-50 dark:bg-gray-900">
201
+ <Outlet />
202
+ </main>
203
+ </div>
204
+ {/* Side panel - renders in flex layout, not overlay */}
205
+ <SidePanel />
173
206
  </div>
174
- <SidePanel />
175
207
  </SidePanelProvider>
176
208
  </ProgressProvider>
177
209
  </div>
@@ -90,7 +90,7 @@ function MilestoneDetailPage() {
90
90
  ]
91
91
 
92
92
  return (
93
- <div className="p-6 max-w-4xl">
93
+ <div className="p-4 lg:p-6 max-w-4xl">
94
94
  <Breadcrumb
95
95
  items={[
96
96
  { label: 'Milestones', href: '/milestones' },
@@ -98,7 +98,7 @@ function MilestoneDetailPage() {
98
98
  ]}
99
99
  />
100
100
 
101
- <h1 className="text-xl font-semibold text-gray-100 dark:text-gray-100 mb-3">{formatMilestoneName(milestone)}</h1>
101
+ <h1 className="text-lg lg:text-xl font-semibold text-gray-100 dark:text-gray-100 mb-3">{formatMilestoneName(milestone)}</h1>
102
102
 
103
103
  <div className="flex items-center gap-3 mb-4">
104
104
  <div className="flex-1 max-w-xs">
@@ -119,7 +119,7 @@ function TaskDetailPage() {
119
119
  ]
120
120
 
121
121
  return (
122
- <div className="p-6 max-w-4xl">
122
+ <div className="p-4 lg:p-6 max-w-4xl">
123
123
  <Breadcrumb
124
124
  items={[
125
125
  { label: 'Milestones', href: '/milestones' },
@@ -128,7 +128,7 @@ function TaskDetailPage() {
128
128
  ]}
129
129
  />
130
130
 
131
- <h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">{formatTaskName(task)}</h1>
131
+ <h1 className="text-lg lg:text-xl font-semibold text-gray-900 dark:text-gray-100 mb-3">{formatTaskName(task)}</h1>
132
132
 
133
133
  <div className="flex items-center gap-2 mb-4">
134
134
  <PriorityBadge priority={task.priority} />
@@ -153,12 +153,12 @@ function TaskDetailPage() {
153
153
 
154
154
  {/* Prev / Next navigation */}
155
155
  {(siblings.prev || siblings.next) && (
156
- <div className="mt-8 flex items-center justify-between border-t border-gray-200 dark:border-gray-800 pt-4">
156
+ <div className="mt-8 flex items-center justify-between gap-4 border-t border-gray-200 dark:border-gray-800 pt-4">
157
157
  {siblings.prev ? (
158
158
  <Link
159
159
  to="/tasks/$taskId"
160
160
  params={{ taskId: siblings.prev.id }}
161
- className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
161
+ className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors truncate"
162
162
  >
163
163
  ← {formatTaskName(siblings.prev)}
164
164
  </Link>
@@ -169,7 +169,7 @@ function TaskDetailPage() {
169
169
  <Link
170
170
  to="/tasks/$taskId"
171
171
  params={{ taskId: siblings.next.id }}
172
- className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
172
+ className="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors truncate text-right"
173
173
  >
174
174
  {formatTaskName(siblings.next)} →
175
175
  </Link>