@prmichaelsen/acp-visualizer 0.6.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -1,15 +1,75 @@
1
1
  import ReactMarkdown from 'react-markdown'
2
2
  import rehypeHighlight from 'rehype-highlight'
3
+ import { Link } from '@tanstack/react-router'
4
+ import type { ProgressData } from '../lib/types'
3
5
 
4
6
  interface MarkdownContentProps {
5
7
  content: string
6
8
  className?: string
9
+ basePath?: string
10
+ linkMap?: Record<string, string>
7
11
  }
8
12
 
9
- export function MarkdownContent({ content, className }: MarkdownContentProps) {
13
+ /**
14
+ * Resolve a relative path against a base file path.
15
+ * e.g. resolvePath("agent/milestones/milestone-3.md", "../tasks/task-11.md")
16
+ * → "agent/tasks/task-11.md"
17
+ */
18
+ export function resolvePath(base: string, relative: string): string {
19
+ const dir = base.split('/').slice(0, -1)
20
+ const relParts = relative.split('/')
21
+ const result = [...dir]
22
+ for (const part of relParts) {
23
+ if (part === '..') result.pop()
24
+ else if (part !== '.') result.push(part)
25
+ }
26
+ return result.join('/')
27
+ }
28
+
29
+ /**
30
+ * Build a map from file paths to visualizer routes.
31
+ */
32
+ export function buildLinkMap(data: ProgressData): Record<string, string> {
33
+ const map: Record<string, string> = {}
34
+ for (const ms of data.milestones) {
35
+ for (const task of data.tasks[ms.id] || []) {
36
+ if (task.file) {
37
+ map[task.file] = `/tasks/${task.id}`
38
+ }
39
+ }
40
+ }
41
+ return map
42
+ }
43
+
44
+ function createMarkdownLink(basePath: string, linkMap: Record<string, string>) {
45
+ return function MarkdownLink({
46
+ href,
47
+ children,
48
+ ...props
49
+ }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
50
+ if (href && !href.startsWith('http') && !href.startsWith('#')) {
51
+ const resolved = resolvePath(basePath, href)
52
+ const route = linkMap[resolved]
53
+ if (route) {
54
+ return <Link to={route}>{children}</Link>
55
+ }
56
+ }
57
+ return (
58
+ <a href={href} {...props}>
59
+ {children}
60
+ </a>
61
+ )
62
+ }
63
+ }
64
+
65
+ export function MarkdownContent({ content, className, basePath, linkMap }: MarkdownContentProps) {
66
+ const components =
67
+ basePath && linkMap
68
+ ? { a: createMarkdownLink(basePath, linkMap) }
69
+ : undefined
70
+
10
71
  return (
11
- <ReactMarkdown
12
- rehypePlugins={[rehypeHighlight]}
72
+ <div
13
73
  className={`prose prose-invert prose-sm max-w-none
14
74
  prose-pre:bg-gray-900 prose-pre:border prose-pre:border-gray-800
15
75
  prose-code:bg-gray-800 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-gray-200
@@ -23,7 +83,9 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
23
83
  prose-blockquote:border-gray-700 prose-blockquote:text-gray-400
24
84
  ${className ?? ''}`}
25
85
  >
26
- {content}
27
- </ReactMarkdown>
86
+ <ReactMarkdown rehypePlugins={[rehypeHighlight]} components={components}>
87
+ {content}
88
+ </ReactMarkdown>
89
+ </div>
28
90
  )
29
91
  }
@@ -14,6 +14,8 @@ import { Route as SearchRouteImport } from './routes/search'
14
14
  import { Route as MilestonesRouteImport } from './routes/milestones'
15
15
  import { Route as ActivityRouteImport } from './routes/activity'
16
16
  import { Route as IndexRouteImport } from './routes/index'
17
+ import { Route as TasksIndexRouteImport } from './routes/tasks.index'
18
+ import { Route as MilestonesIndexRouteImport } from './routes/milestones.index'
17
19
  import { Route as TasksTaskIdRouteImport } from './routes/tasks.$taskId'
18
20
  import { Route as MilestonesMilestoneIdRouteImport } from './routes/milestones.$milestoneId'
19
21
  import { Route as ApiWatchRouteImport } from './routes/api/watch'
@@ -43,6 +45,16 @@ const IndexRoute = IndexRouteImport.update({
43
45
  path: '/',
44
46
  getParentRoute: () => rootRouteImport,
45
47
  } as any)
48
+ const TasksIndexRoute = TasksIndexRouteImport.update({
49
+ id: '/',
50
+ path: '/',
51
+ getParentRoute: () => TasksRoute,
52
+ } as any)
53
+ const MilestonesIndexRoute = MilestonesIndexRouteImport.update({
54
+ id: '/',
55
+ path: '/',
56
+ getParentRoute: () => MilestonesRoute,
57
+ } as any)
46
58
  const TasksTaskIdRoute = TasksTaskIdRouteImport.update({
47
59
  id: '/$taskId',
48
60
  path: '/$taskId',
@@ -68,16 +80,18 @@ export interface FileRoutesByFullPath {
68
80
  '/api/watch': typeof ApiWatchRoute
69
81
  '/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
70
82
  '/tasks/$taskId': typeof TasksTaskIdRoute
83
+ '/milestones/': typeof MilestonesIndexRoute
84
+ '/tasks/': typeof TasksIndexRoute
71
85
  }
72
86
  export interface FileRoutesByTo {
73
87
  '/': typeof IndexRoute
74
88
  '/activity': typeof ActivityRoute
75
- '/milestones': typeof MilestonesRouteWithChildren
76
89
  '/search': typeof SearchRoute
77
- '/tasks': typeof TasksRouteWithChildren
78
90
  '/api/watch': typeof ApiWatchRoute
79
91
  '/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
80
92
  '/tasks/$taskId': typeof TasksTaskIdRoute
93
+ '/milestones': typeof MilestonesIndexRoute
94
+ '/tasks': typeof TasksIndexRoute
81
95
  }
82
96
  export interface FileRoutesById {
83
97
  __root__: typeof rootRouteImport
@@ -89,6 +103,8 @@ export interface FileRoutesById {
89
103
  '/api/watch': typeof ApiWatchRoute
90
104
  '/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
91
105
  '/tasks/$taskId': typeof TasksTaskIdRoute
106
+ '/milestones/': typeof MilestonesIndexRoute
107
+ '/tasks/': typeof TasksIndexRoute
92
108
  }
93
109
  export interface FileRouteTypes {
94
110
  fileRoutesByFullPath: FileRoutesByFullPath
@@ -101,16 +117,18 @@ export interface FileRouteTypes {
101
117
  | '/api/watch'
102
118
  | '/milestones/$milestoneId'
103
119
  | '/tasks/$taskId'
120
+ | '/milestones/'
121
+ | '/tasks/'
104
122
  fileRoutesByTo: FileRoutesByTo
105
123
  to:
106
124
  | '/'
107
125
  | '/activity'
108
- | '/milestones'
109
126
  | '/search'
110
- | '/tasks'
111
127
  | '/api/watch'
112
128
  | '/milestones/$milestoneId'
113
129
  | '/tasks/$taskId'
130
+ | '/milestones'
131
+ | '/tasks'
114
132
  id:
115
133
  | '__root__'
116
134
  | '/'
@@ -121,6 +139,8 @@ export interface FileRouteTypes {
121
139
  | '/api/watch'
122
140
  | '/milestones/$milestoneId'
123
141
  | '/tasks/$taskId'
142
+ | '/milestones/'
143
+ | '/tasks/'
124
144
  fileRoutesById: FileRoutesById
125
145
  }
126
146
  export interface RootRouteChildren {
@@ -169,6 +189,20 @@ declare module '@tanstack/react-router' {
169
189
  preLoaderRoute: typeof IndexRouteImport
170
190
  parentRoute: typeof rootRouteImport
171
191
  }
192
+ '/tasks/': {
193
+ id: '/tasks/'
194
+ path: '/'
195
+ fullPath: '/tasks/'
196
+ preLoaderRoute: typeof TasksIndexRouteImport
197
+ parentRoute: typeof TasksRoute
198
+ }
199
+ '/milestones/': {
200
+ id: '/milestones/'
201
+ path: '/'
202
+ fullPath: '/milestones/'
203
+ preLoaderRoute: typeof MilestonesIndexRouteImport
204
+ parentRoute: typeof MilestonesRoute
205
+ }
172
206
  '/tasks/$taskId': {
173
207
  id: '/tasks/$taskId'
174
208
  path: '/$taskId'
@@ -195,10 +229,12 @@ declare module '@tanstack/react-router' {
195
229
 
196
230
  interface MilestonesRouteChildren {
197
231
  MilestonesMilestoneIdRoute: typeof MilestonesMilestoneIdRoute
232
+ MilestonesIndexRoute: typeof MilestonesIndexRoute
198
233
  }
199
234
 
200
235
  const MilestonesRouteChildren: MilestonesRouteChildren = {
201
236
  MilestonesMilestoneIdRoute: MilestonesMilestoneIdRoute,
237
+ MilestonesIndexRoute: MilestonesIndexRoute,
202
238
  }
203
239
 
204
240
  const MilestonesRouteWithChildren = MilestonesRoute._addFileChildren(
@@ -207,10 +243,12 @@ const MilestonesRouteWithChildren = MilestonesRoute._addFileChildren(
207
243
 
208
244
  interface TasksRouteChildren {
209
245
  TasksTaskIdRoute: typeof TasksTaskIdRoute
246
+ TasksIndexRoute: typeof TasksIndexRoute
210
247
  }
211
248
 
212
249
  const TasksRouteChildren: TasksRouteChildren = {
213
250
  TasksTaskIdRoute: TasksTaskIdRoute,
251
+ TasksIndexRoute: TasksIndexRoute,
214
252
  }
215
253
 
216
254
  const TasksRouteWithChildren = TasksRoute._addFileChildren(TasksRouteChildren)
@@ -1,11 +1,11 @@
1
1
  import { createFileRoute, Link } from '@tanstack/react-router'
2
- import { useState, useEffect } from 'react'
2
+ import { useState, useEffect, useMemo } from 'react'
3
3
  import { useProgressData } from '../contexts/ProgressContext'
4
4
  import { Breadcrumb } from '../components/Breadcrumb'
5
5
  import { DetailHeader } from '../components/DetailHeader'
6
6
  import { ProgressBar } from '../components/ProgressBar'
7
7
  import { StatusDot } from '../components/StatusDot'
8
- import { MarkdownContent } from '../components/MarkdownContent'
8
+ import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
9
9
  import { getMarkdownContent, resolveMilestoneFile } from '../services/markdown.service'
10
10
  import type { MarkdownResult, ResolveFileResult } from '../services/markdown.service'
11
11
 
@@ -29,10 +29,12 @@ function MilestoneDetailPage() {
29
29
  const data = useProgressData()
30
30
  const [markdown, setMarkdown] = useState<string | null>(null)
31
31
  const [markdownError, setMarkdownError] = useState<string | null>(null)
32
+ const [markdownFilePath, setMarkdownFilePath] = useState<string | null>(null)
32
33
  const [loading, setLoading] = useState(true)
33
34
 
34
35
  const milestone = data?.milestones.find((m) => m.id === milestoneId)
35
36
  const tasks = data?.tasks[milestoneId] || []
37
+ const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
36
38
 
37
39
  useEffect(() => {
38
40
  if (!milestoneId) return
@@ -40,6 +42,7 @@ function MilestoneDetailPage() {
40
42
  setLoading(true)
41
43
  setMarkdown(null)
42
44
  setMarkdownError(null)
45
+ setMarkdownFilePath(null)
43
46
 
44
47
  const github = getGitHubParams()
45
48
 
@@ -51,6 +54,7 @@ function MilestoneDetailPage() {
51
54
  return
52
55
  }
53
56
 
57
+ setMarkdownFilePath(resolveResult.filePath)
54
58
  return getMarkdownContent({ data: { filePath: resolveResult.filePath, github } })
55
59
  .then((mdResult: MarkdownResult) => {
56
60
  if (mdResult.ok) {
@@ -111,7 +115,7 @@ function MilestoneDetailPage() {
111
115
  {loading ? (
112
116
  <p className="text-sm text-gray-600">Loading document...</p>
113
117
  ) : markdown ? (
114
- <MarkdownContent content={markdown} />
118
+ <MarkdownContent content={markdown} basePath={markdownFilePath ?? undefined} linkMap={linkMap} />
115
119
  ) : markdownError ? (
116
120
  <div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
117
121
  No document found — {markdownError}
@@ -0,0 +1,67 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { useState, lazy, Suspense } from 'react'
3
+ import { MilestoneTable } from '../components/MilestoneTable'
4
+ import { MilestoneTree } from '../components/MilestoneTree'
5
+ import { MilestoneKanban } from '../components/MilestoneKanban'
6
+ import { MilestoneGantt } from '../components/MilestoneGantt'
7
+ import { ViewToggle, type ViewMode } from '../components/ViewToggle'
8
+ import { FilterBar } from '../components/FilterBar'
9
+ import { SearchInput } from '../components/SearchInput'
10
+ import { useFilteredData } from '../lib/useFilteredData'
11
+ import { useProgressData } from '../contexts/ProgressContext'
12
+ import type { Status } from '../lib/types'
13
+
14
+ // Lazy-load DependencyGraph to keep dagre out of the SSR bundle
15
+ // (dagre uses CommonJS require() which fails on Cloudflare Workers)
16
+ const DependencyGraph = lazy(() => import('../components/DependencyGraph').then(m => ({ default: m.DependencyGraph })))
17
+
18
+ export const Route = createFileRoute('/milestones/')({
19
+ component: MilestonesPage,
20
+ })
21
+
22
+ function MilestonesPage() {
23
+ const progressData = useProgressData()
24
+ const [view, setView] = useState<ViewMode>('table')
25
+ const [status, setStatus] = useState<Status | 'all'>('all')
26
+ const [search, setSearch] = useState('')
27
+
28
+ const filtered = useFilteredData(progressData, { status, search })
29
+
30
+ if (!filtered) {
31
+ return (
32
+ <div className="p-6">
33
+ <p className="text-gray-600 text-sm">No data loaded</p>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ return (
39
+ <div className="p-6">
40
+ <div className="flex items-center justify-between mb-4">
41
+ <h2 className="text-lg font-semibold">Milestones</h2>
42
+ <ViewToggle value={view} onChange={setView} />
43
+ </div>
44
+ {view !== 'kanban' && (
45
+ <div className="flex items-center gap-3 mb-4">
46
+ <FilterBar status={status} onStatusChange={setStatus} />
47
+ <div className="w-64">
48
+ <SearchInput value={search} onChange={setSearch} placeholder="Filter milestones..." />
49
+ </div>
50
+ </div>
51
+ )}
52
+ {view === 'table' ? (
53
+ <MilestoneTable milestones={filtered.milestones} tasks={filtered.tasks} />
54
+ ) : view === 'tree' ? (
55
+ <MilestoneTree milestones={filtered.milestones} tasks={filtered.tasks} />
56
+ ) : view === 'kanban' ? (
57
+ <MilestoneKanban milestones={filtered.milestones} tasks={filtered.tasks} />
58
+ ) : view === 'gantt' ? (
59
+ <MilestoneGantt milestones={filtered.milestones} tasks={filtered.tasks} />
60
+ ) : (
61
+ <Suspense fallback={<p className="text-gray-500 text-sm">Loading graph...</p>}>
62
+ <DependencyGraph data={filtered} />
63
+ </Suspense>
64
+ )}
65
+ </div>
66
+ )
67
+ }
@@ -1,67 +1,9 @@
1
- import { createFileRoute } from '@tanstack/react-router'
2
- import { useState, lazy, Suspense } from 'react'
3
- import { MilestoneTable } from '../components/MilestoneTable'
4
- import { MilestoneTree } from '../components/MilestoneTree'
5
- import { MilestoneKanban } from '../components/MilestoneKanban'
6
- import { MilestoneGantt } from '../components/MilestoneGantt'
7
- import { ViewToggle, type ViewMode } from '../components/ViewToggle'
8
- import { FilterBar } from '../components/FilterBar'
9
- import { SearchInput } from '../components/SearchInput'
10
- import { useFilteredData } from '../lib/useFilteredData'
11
- import { useProgressData } from '../contexts/ProgressContext'
12
- import type { Status } from '../lib/types'
13
-
14
- // Lazy-load DependencyGraph to keep dagre out of the SSR bundle
15
- // (dagre uses CommonJS require() which fails on Cloudflare Workers)
16
- const DependencyGraph = lazy(() => import('../components/DependencyGraph').then(m => ({ default: m.DependencyGraph })))
1
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
17
2
 
18
3
  export const Route = createFileRoute('/milestones')({
19
- component: MilestonesPage,
4
+ component: MilestonesLayout,
20
5
  })
21
6
 
22
- function MilestonesPage() {
23
- const progressData = useProgressData()
24
- const [view, setView] = useState<ViewMode>('table')
25
- const [status, setStatus] = useState<Status | 'all'>('all')
26
- const [search, setSearch] = useState('')
27
-
28
- const filtered = useFilteredData(progressData, { status, search })
29
-
30
- if (!filtered) {
31
- return (
32
- <div className="p-6">
33
- <p className="text-gray-600 text-sm">No data loaded</p>
34
- </div>
35
- )
36
- }
37
-
38
- return (
39
- <div className="p-6">
40
- <div className="flex items-center justify-between mb-4">
41
- <h2 className="text-lg font-semibold">Milestones</h2>
42
- <ViewToggle value={view} onChange={setView} />
43
- </div>
44
- {view !== 'kanban' && (
45
- <div className="flex items-center gap-3 mb-4">
46
- <FilterBar status={status} onStatusChange={setStatus} />
47
- <div className="w-64">
48
- <SearchInput value={search} onChange={setSearch} placeholder="Filter milestones..." />
49
- </div>
50
- </div>
51
- )}
52
- {view === 'table' ? (
53
- <MilestoneTable milestones={filtered.milestones} tasks={filtered.tasks} />
54
- ) : view === 'tree' ? (
55
- <MilestoneTree milestones={filtered.milestones} tasks={filtered.tasks} />
56
- ) : view === 'kanban' ? (
57
- <MilestoneKanban milestones={filtered.milestones} tasks={filtered.tasks} />
58
- ) : view === 'gantt' ? (
59
- <MilestoneGantt milestones={filtered.milestones} tasks={filtered.tasks} />
60
- ) : (
61
- <Suspense fallback={<p className="text-gray-500 text-sm">Loading graph...</p>}>
62
- <DependencyGraph data={filtered} />
63
- </Suspense>
64
- )}
65
- </div>
66
- )
7
+ function MilestonesLayout() {
8
+ return <Outlet />
67
9
  }
@@ -3,7 +3,7 @@ import { useState, useEffect, useMemo } from 'react'
3
3
  import { useProgressData } from '../contexts/ProgressContext'
4
4
  import { Breadcrumb } from '../components/Breadcrumb'
5
5
  import { DetailHeader } from '../components/DetailHeader'
6
- import { MarkdownContent } from '../components/MarkdownContent'
6
+ import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
7
7
  import { getMarkdownContent } from '../services/markdown.service'
8
8
  import { resolveTaskFile } from '../services/markdown.service'
9
9
  import type { MarkdownResult } from '../services/markdown.service'
@@ -83,6 +83,9 @@ function TaskDetailPage() {
83
83
  })
84
84
  }, [task])
85
85
 
86
+ const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
87
+ const taskFilePath = useMemo(() => resolveTaskFile(task), [task])
88
+
86
89
  if (!data || !task || !milestone) {
87
90
  return (
88
91
  <div className="p-6">
@@ -130,7 +133,7 @@ function TaskDetailPage() {
130
133
  {loading ? (
131
134
  <p className="text-sm text-gray-600">Loading document...</p>
132
135
  ) : markdown ? (
133
- <MarkdownContent content={markdown} />
136
+ <MarkdownContent content={markdown} basePath={taskFilePath ?? undefined} linkMap={linkMap} />
134
137
  ) : markdownError ? (
135
138
  <div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
136
139
  No document found — {markdownError}
@@ -0,0 +1,66 @@
1
+ import { createFileRoute, Link } from '@tanstack/react-router'
2
+ import { StatusDot } from '../components/StatusDot'
3
+ import { ExtraFieldsBadge } from '../components/ExtraFieldsBadge'
4
+ import { useProgressData } from '../contexts/ProgressContext'
5
+ import type { Task } from '../lib/types'
6
+
7
+ export const Route = createFileRoute('/tasks/')({
8
+ component: TasksPage,
9
+ })
10
+
11
+ function TasksPage() {
12
+ const progressData = useProgressData()
13
+
14
+ if (!progressData) {
15
+ return (
16
+ <div className="p-6">
17
+ <p className="text-gray-600 text-sm">No data loaded</p>
18
+ </div>
19
+ )
20
+ }
21
+
22
+ const allTasks: Array<Task & { milestoneName: string }> = []
23
+ for (const milestone of progressData.milestones) {
24
+ const tasks = progressData.tasks[milestone.id] || []
25
+ for (const task of tasks) {
26
+ allTasks.push({ ...task, milestoneName: milestone.name })
27
+ }
28
+ }
29
+
30
+ return (
31
+ <div className="p-6">
32
+ <h2 className="text-lg font-semibold mb-4">
33
+ All Tasks ({allTasks.length})
34
+ </h2>
35
+ <div className="border border-gray-800 rounded-lg overflow-hidden">
36
+ {allTasks.map((task) => (
37
+ <Link
38
+ key={task.id}
39
+ to="/tasks/$taskId"
40
+ params={{ taskId: task.id }}
41
+ className="flex items-center gap-3 px-4 py-2.5 border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
42
+ >
43
+ <StatusDot status={task.status} />
44
+ <span
45
+ className={`flex-1 text-sm ${
46
+ task.status === 'completed' ? 'text-gray-500' : 'text-gray-200'
47
+ }`}
48
+ >
49
+ {task.name}
50
+ </span>
51
+ <span className="text-xs text-gray-600">{task.milestoneName}</span>
52
+ <span className="text-xs text-gray-500 font-mono w-8 text-right">
53
+ {task.estimated_hours}h
54
+ </span>
55
+ <ExtraFieldsBadge fields={task.extra} />
56
+ </Link>
57
+ ))}
58
+ {allTasks.length === 0 && (
59
+ <div className="px-4 py-6 text-center">
60
+ <p className="text-gray-600 text-sm">No tasks defined</p>
61
+ </div>
62
+ )}
63
+ </div>
64
+ </div>
65
+ )
66
+ }
@@ -1,64 +1,9 @@
1
- import { createFileRoute } from '@tanstack/react-router'
2
- import { StatusDot } from '../components/StatusDot'
3
- import { ExtraFieldsBadge } from '../components/ExtraFieldsBadge'
4
- import { useProgressData } from '../contexts/ProgressContext'
5
- import type { Task } from '../lib/types'
1
+ import { createFileRoute, Outlet } from '@tanstack/react-router'
6
2
 
7
3
  export const Route = createFileRoute('/tasks')({
8
- component: TasksPage,
4
+ component: TasksLayout,
9
5
  })
10
6
 
11
- function TasksPage() {
12
- const progressData = useProgressData()
13
-
14
- if (!progressData) {
15
- return (
16
- <div className="p-6">
17
- <p className="text-gray-600 text-sm">No data loaded</p>
18
- </div>
19
- )
20
- }
21
-
22
- const allTasks: Array<Task & { milestoneName: string }> = []
23
- for (const milestone of progressData.milestones) {
24
- const tasks = progressData.tasks[milestone.id] || []
25
- for (const task of tasks) {
26
- allTasks.push({ ...task, milestoneName: milestone.name })
27
- }
28
- }
29
-
30
- return (
31
- <div className="p-6">
32
- <h2 className="text-lg font-semibold mb-4">
33
- All Tasks ({allTasks.length})
34
- </h2>
35
- <div className="border border-gray-800 rounded-lg overflow-hidden">
36
- {allTasks.map((task) => (
37
- <div
38
- key={task.id}
39
- className="flex items-center gap-3 px-4 py-2.5 border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
40
- >
41
- <StatusDot status={task.status} />
42
- <span
43
- className={`flex-1 text-sm ${
44
- task.status === 'completed' ? 'text-gray-500' : 'text-gray-200'
45
- }`}
46
- >
47
- {task.name}
48
- </span>
49
- <span className="text-xs text-gray-600">{task.milestoneName}</span>
50
- <span className="text-xs text-gray-500 font-mono w-8 text-right">
51
- {task.estimated_hours}h
52
- </span>
53
- <ExtraFieldsBadge fields={task.extra} />
54
- </div>
55
- ))}
56
- {allTasks.length === 0 && (
57
- <div className="px-4 py-6 text-center">
58
- <p className="text-gray-600 text-sm">No tasks defined</p>
59
- </div>
60
- )}
61
- </div>
62
- </div>
63
- )
7
+ function TasksLayout() {
8
+ return <Outlet />
64
9
  }