@prmichaelsen/acp-visualizer 0.8.1 → 0.9.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 +1 -1
- package/src/components/DocumentDetail.tsx +100 -0
- package/src/components/DocumentList.tsx +90 -0
- package/src/components/Header.tsx +23 -6
- package/src/components/MilestoneKanban.tsx +14 -8
- package/src/components/MilestonePreview.tsx +162 -0
- package/src/components/MilestoneTable.tsx +18 -8
- package/src/components/MilestoneTree.tsx +19 -13
- package/src/components/PreviewButton.tsx +33 -0
- package/src/components/PriorityBadge.tsx +20 -0
- package/src/components/SidePanel.tsx +45 -0
- package/src/components/Sidebar.tsx +11 -8
- package/src/components/TaskList.tsx +9 -5
- package/src/components/TaskPreview.tsx +147 -0
- package/src/contexts/SidePanelContext.tsx +50 -0
- package/src/lib/types.ts +8 -0
- package/src/lib/useTheme.ts +27 -0
- package/src/lib/yaml-loader.ts +49 -13
- package/src/routeTree.gen.ts +211 -0
- package/src/routes/__root.tsx +14 -9
- package/src/routes/designs.$slug.tsx +18 -0
- package/src/routes/designs.index.tsx +10 -0
- package/src/routes/designs.tsx +9 -0
- package/src/routes/milestones.$milestoneId.tsx +6 -0
- package/src/routes/patterns.$slug.tsx +18 -0
- package/src/routes/patterns.index.tsx +10 -0
- package/src/routes/patterns.tsx +9 -0
- package/src/routes/reports.$slug.tsx +18 -0
- package/src/routes/reports.index.tsx +10 -0
- package/src/routes/reports.tsx +9 -0
- package/src/routes/tasks.$taskId.tsx +11 -1
- package/src/services/markdown.service.ts +102 -0
- package/src/styles.css +2 -2
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { Breadcrumb } from './Breadcrumb'
|
|
3
|
+
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
4
|
+
import { getMarkdownContent, listAgentDirectory } from '../services/markdown.service'
|
|
5
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
6
|
+
import type { MarkdownResult, AgentFile } from '../services/markdown.service'
|
|
7
|
+
|
|
8
|
+
interface DocumentDetailProps {
|
|
9
|
+
slug: string
|
|
10
|
+
dirPath: string
|
|
11
|
+
sectionLabel: string
|
|
12
|
+
sectionHref: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read ?repo=owner/repo from URL */
|
|
16
|
+
function getGitHubParams(): { owner: string; repo: string } | undefined {
|
|
17
|
+
if (typeof window === 'undefined') return undefined
|
|
18
|
+
const params = new URLSearchParams(window.location.search)
|
|
19
|
+
const repo = params.get('repo')
|
|
20
|
+
if (!repo) return undefined
|
|
21
|
+
const parts = repo.split('/')
|
|
22
|
+
if (parts.length < 2) return undefined
|
|
23
|
+
return { owner: parts[0], repo: parts[1] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DocumentDetail({ slug, dirPath, sectionLabel, sectionHref }: DocumentDetailProps) {
|
|
27
|
+
const data = useProgressData()
|
|
28
|
+
const [markdown, setMarkdown] = useState<string | null>(null)
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
const [loading, setLoading] = useState(true)
|
|
31
|
+
const [filePath, setFilePath] = useState<string | null>(null)
|
|
32
|
+
const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setLoading(true)
|
|
36
|
+
setMarkdown(null)
|
|
37
|
+
setError(null)
|
|
38
|
+
|
|
39
|
+
const github = getGitHubParams()
|
|
40
|
+
const relativePath = `${dirPath}/${slug}.md`
|
|
41
|
+
|
|
42
|
+
// Try the direct path first, fallback to directory listing if needed
|
|
43
|
+
getMarkdownContent({ data: { filePath: relativePath, github } })
|
|
44
|
+
.then((result: MarkdownResult) => {
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
setMarkdown(result.content)
|
|
47
|
+
setFilePath(result.filePath)
|
|
48
|
+
} else {
|
|
49
|
+
// Fallback: search directory for a file containing the slug
|
|
50
|
+
return listAgentDirectory({ data: { dirPath, github } }).then((listResult) => {
|
|
51
|
+
if (listResult.ok) {
|
|
52
|
+
const match = listResult.files.find((f: AgentFile) => f.name === slug)
|
|
53
|
+
if (match) {
|
|
54
|
+
return getMarkdownContent({ data: { filePath: match.relativePath, github } })
|
|
55
|
+
.then((mdResult: MarkdownResult) => {
|
|
56
|
+
if (mdResult.ok) {
|
|
57
|
+
setMarkdown(mdResult.content)
|
|
58
|
+
setFilePath(mdResult.filePath)
|
|
59
|
+
} else {
|
|
60
|
+
setError(mdResult.error)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setError(result.error)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.catch((err: Error) => setError(err.message))
|
|
70
|
+
.finally(() => setLoading(false))
|
|
71
|
+
}, [slug, dirPath])
|
|
72
|
+
|
|
73
|
+
const displayName = slug
|
|
74
|
+
.replace(/^[a-z0-9-]+\./, '')
|
|
75
|
+
.replace(/[-_]/g, ' ')
|
|
76
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="p-6 max-w-4xl">
|
|
80
|
+
<Breadcrumb
|
|
81
|
+
items={[
|
|
82
|
+
{ label: sectionLabel, href: sectionHref },
|
|
83
|
+
{ label: displayName },
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<h1 className="text-xl font-semibold text-gray-100 mb-6">{displayName}</h1>
|
|
88
|
+
|
|
89
|
+
{loading ? (
|
|
90
|
+
<p className="text-sm text-gray-600">Loading document...</p>
|
|
91
|
+
) : markdown ? (
|
|
92
|
+
<MarkdownContent content={markdown} basePath={filePath ?? undefined} linkMap={linkMap} />
|
|
93
|
+
) : error ? (
|
|
94
|
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
|
|
95
|
+
No document found — {error}
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import { useState, useEffect } from 'react'
|
|
3
|
+
import { listAgentDirectory } from '../services/markdown.service'
|
|
4
|
+
import type { AgentFile } from '../services/markdown.service'
|
|
5
|
+
import { FileText } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
interface DocumentListProps {
|
|
8
|
+
title: string
|
|
9
|
+
dirPath: string
|
|
10
|
+
baseTo: string
|
|
11
|
+
github?: { owner: string; repo: string; branch?: string; token?: string }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DocumentList({ title, dirPath, baseTo, github }: DocumentListProps) {
|
|
15
|
+
const [files, setFiles] = useState<AgentFile[]>([])
|
|
16
|
+
const [error, setError] = useState<string | null>(null)
|
|
17
|
+
const [loading, setLoading] = useState(true)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setLoading(true)
|
|
21
|
+
listAgentDirectory({ data: { dirPath, github } })
|
|
22
|
+
.then((result) => {
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
setFiles(result.files)
|
|
25
|
+
} else {
|
|
26
|
+
setError(result.error)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.catch((err: Error) => setError(err.message))
|
|
30
|
+
.finally(() => setLoading(false))
|
|
31
|
+
}, [dirPath, github])
|
|
32
|
+
|
|
33
|
+
if (loading) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="p-6">
|
|
36
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
37
|
+
<p className="text-sm text-gray-600">Loading...</p>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="p-6">
|
|
45
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
46
|
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
|
|
47
|
+
{error}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="p-6">
|
|
56
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
57
|
+
<p className="text-sm text-gray-500">No documents found in <code className="text-gray-400">{dirPath}/</code></p>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="p-6">
|
|
64
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
65
|
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl divide-y divide-gray-800">
|
|
66
|
+
{files.map((file) => (
|
|
67
|
+
<Link
|
|
68
|
+
key={file.name}
|
|
69
|
+
to={baseTo + '/$slug'}
|
|
70
|
+
params={{ slug: file.name }}
|
|
71
|
+
className="flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-800/50 transition-colors first:rounded-t-xl last:rounded-b-xl"
|
|
72
|
+
>
|
|
73
|
+
<FileText className="w-4 h-4 text-gray-500 shrink-0" />
|
|
74
|
+
<span className="text-gray-200">{formatName(file.name)}</span>
|
|
75
|
+
</Link>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
<p className="text-xs text-gray-600 mt-3">{files.length} document{files.length !== 1 ? 's' : ''}</p>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Turn "local.dashboard-layout-routing" into "Dashboard Layout Routing" */
|
|
84
|
+
function formatName(name: string): string {
|
|
85
|
+
// Strip common prefixes like "local." or "core-sdk."
|
|
86
|
+
const stripped = name.replace(/^[a-z0-9-]+\./, '')
|
|
87
|
+
return stripped
|
|
88
|
+
.replace(/[-_]/g, ' ')
|
|
89
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
90
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Moon, Sun } from 'lucide-react'
|
|
1
2
|
import { StatusBadge } from './StatusBadge'
|
|
2
3
|
import { ProgressBar } from './ProgressBar'
|
|
4
|
+
import { useTheme } from '../lib/useTheme'
|
|
3
5
|
import type { ProgressData } from '../lib/types'
|
|
4
6
|
|
|
5
7
|
interface HeaderProps {
|
|
@@ -7,16 +9,31 @@ interface HeaderProps {
|
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export function Header({ data }: HeaderProps) {
|
|
12
|
+
const { theme, toggleTheme } = useTheme()
|
|
13
|
+
|
|
10
14
|
if (!data) return null
|
|
11
15
|
|
|
12
16
|
return (
|
|
13
|
-
<header className="h-14 border-b border-gray-800 flex items-center px-6 gap-4 shrink-0">
|
|
14
|
-
<h1 className="text-sm font-medium text-gray-200">{data.project.name}</h1>
|
|
15
|
-
<span className="text-xs text-gray-500 font-mono">v{data.project.version}</span>
|
|
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>
|
|
16
20
|
<StatusBadge status={data.project.status} />
|
|
17
|
-
<div className="ml-auto flex items-center gap-
|
|
18
|
-
<
|
|
19
|
-
|
|
21
|
+
<div className="ml-auto flex items-center gap-4">
|
|
22
|
+
<div className="flex items-center gap-3 w-48">
|
|
23
|
+
<ProgressBar value={data.progress.overall} size="sm" />
|
|
24
|
+
<span className="text-xs text-gray-600 dark:text-gray-400 font-mono">{data.progress.overall}%</span>
|
|
25
|
+
</div>
|
|
26
|
+
<button
|
|
27
|
+
onClick={toggleTheme}
|
|
28
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
29
|
+
aria-label="Toggle theme"
|
|
30
|
+
>
|
|
31
|
+
{theme === 'dark' ? (
|
|
32
|
+
<Moon className="w-4 h-4 text-gray-400" />
|
|
33
|
+
) : (
|
|
34
|
+
<Sun className="w-4 h-4 text-gray-600" />
|
|
35
|
+
)}
|
|
36
|
+
</button>
|
|
20
37
|
</div>
|
|
21
38
|
</header>
|
|
22
39
|
)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Link } from '@tanstack/react-router'
|
|
2
2
|
import { StatusBadge } from './StatusBadge'
|
|
3
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
3
4
|
import { ProgressBar } from './ProgressBar'
|
|
5
|
+
import { PreviewButton } from './PreviewButton'
|
|
4
6
|
import { TaskList } from './TaskList'
|
|
5
7
|
import type { Milestone, Task, Status } from '../lib/types'
|
|
6
8
|
import { useState } from 'react'
|
|
@@ -28,15 +30,19 @@ function KanbanCard({
|
|
|
28
30
|
const [expanded, setExpanded] = useState(false)
|
|
29
31
|
|
|
30
32
|
return (
|
|
31
|
-
<div className="bg-gray-900/50 border border-gray-800 rounded-lg p-3 hover:border-gray-700 transition-colors">
|
|
33
|
+
<div className="bg-gray-100 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800 rounded-lg p-3 hover:border-gray-300 dark:hover:border-gray-700 transition-colors group">
|
|
32
34
|
<div className="flex items-start justify-between gap-2 mb-2">
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
<div className="flex items-center gap-2 flex-1">
|
|
36
|
+
<Link
|
|
37
|
+
to="/milestones/$milestoneId"
|
|
38
|
+
params={{ milestoneId: milestone.id }}
|
|
39
|
+
className="text-sm font-medium leading-tight text-gray-900 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
|
40
|
+
>
|
|
41
|
+
{milestone.name}
|
|
42
|
+
</Link>
|
|
43
|
+
<PreviewButton type="milestone" id={milestone.id} />
|
|
44
|
+
</div>
|
|
45
|
+
<PriorityBadge priority={milestone.priority} />
|
|
40
46
|
</div>
|
|
41
47
|
<div className="flex items-center gap-2 mb-2">
|
|
42
48
|
<div className="flex-1">
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
3
|
+
import { ExternalLink } from 'lucide-react'
|
|
4
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
5
|
+
import { DetailHeader } from './DetailHeader'
|
|
6
|
+
import { ProgressBar } from './ProgressBar'
|
|
7
|
+
import { StatusDot } from './StatusDot'
|
|
8
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
9
|
+
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
10
|
+
import { getMarkdownContent, resolveMilestoneFile } from '../services/markdown.service'
|
|
11
|
+
import type { MarkdownResult, ResolveFileResult } from '../services/markdown.service'
|
|
12
|
+
|
|
13
|
+
interface MilestonePreviewProps {
|
|
14
|
+
milestoneId: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getGitHubParams(): { owner: string; repo: string } | undefined {
|
|
18
|
+
if (typeof window === 'undefined') return undefined
|
|
19
|
+
const params = new URLSearchParams(window.location.search)
|
|
20
|
+
const repo = params.get('repo')
|
|
21
|
+
if (!repo) return undefined
|
|
22
|
+
const parts = repo.split('/')
|
|
23
|
+
if (parts.length < 2) return undefined
|
|
24
|
+
return { owner: parts[0], repo: parts[1] }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function MilestonePreview({ milestoneId }: MilestonePreviewProps) {
|
|
28
|
+
const data = useProgressData()
|
|
29
|
+
const [markdown, setMarkdown] = useState<string | null>(null)
|
|
30
|
+
const [markdownError, setMarkdownError] = useState<string | null>(null)
|
|
31
|
+
const [markdownFilePath, setMarkdownFilePath] = useState<string | null>(null)
|
|
32
|
+
const [loading, setLoading] = useState(true)
|
|
33
|
+
|
|
34
|
+
const milestone = data?.milestones.find((m) => m.id === milestoneId)
|
|
35
|
+
const tasks = data?.tasks[milestoneId] || []
|
|
36
|
+
const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!milestoneId) return
|
|
40
|
+
|
|
41
|
+
setLoading(true)
|
|
42
|
+
setMarkdown(null)
|
|
43
|
+
setMarkdownError(null)
|
|
44
|
+
setMarkdownFilePath(null)
|
|
45
|
+
|
|
46
|
+
const github = getGitHubParams()
|
|
47
|
+
|
|
48
|
+
resolveMilestoneFile({ data: { milestoneId, github } })
|
|
49
|
+
.then((resolveResult: ResolveFileResult) => {
|
|
50
|
+
if (!resolveResult.ok) {
|
|
51
|
+
setMarkdownError(resolveResult.error)
|
|
52
|
+
setLoading(false)
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setMarkdownFilePath(resolveResult.filePath)
|
|
57
|
+
return getMarkdownContent({ data: { filePath: resolveResult.filePath, github } })
|
|
58
|
+
.then((mdResult: MarkdownResult) => {
|
|
59
|
+
if (mdResult.ok) {
|
|
60
|
+
setMarkdown(mdResult.content)
|
|
61
|
+
} else {
|
|
62
|
+
setMarkdownError(mdResult.error)
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
.catch((err: Error) => {
|
|
67
|
+
setMarkdownError(err.message)
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
setLoading(false)
|
|
71
|
+
})
|
|
72
|
+
}, [milestoneId])
|
|
73
|
+
|
|
74
|
+
if (!data || !milestone) {
|
|
75
|
+
return (
|
|
76
|
+
<div className="text-center py-8">
|
|
77
|
+
<p className="text-gray-500 dark:text-gray-400 text-sm">Milestone not found: {milestoneId}</p>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fields = [
|
|
83
|
+
...(milestone.started ? [{ label: 'Started', value: milestone.started }] : []),
|
|
84
|
+
...(milestone.completed ? [{ label: 'Completed', value: milestone.completed }] : []),
|
|
85
|
+
{ label: 'Est', value: `${milestone.estimated_weeks} week${milestone.estimated_weeks === '1' ? '' : 's'}` },
|
|
86
|
+
{ label: 'Tasks', value: `${milestone.tasks_completed}/${milestone.tasks_total}` },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div>
|
|
91
|
+
<div className="flex items-start justify-between mb-4">
|
|
92
|
+
<h1 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{milestone.name}</h1>
|
|
93
|
+
<Link
|
|
94
|
+
to="/milestones/$milestoneId"
|
|
95
|
+
params={{ milestoneId }}
|
|
96
|
+
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
97
|
+
title="Open full view"
|
|
98
|
+
>
|
|
99
|
+
<ExternalLink className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
100
|
+
</Link>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div className="flex items-center gap-3 mb-4">
|
|
104
|
+
<div className="flex-1 max-w-xs">
|
|
105
|
+
<ProgressBar value={milestone.progress} size="sm" />
|
|
106
|
+
</div>
|
|
107
|
+
<span className="text-xs text-gray-600 dark:text-gray-500">{milestone.progress}%</span>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div className="flex items-center gap-2 mb-4">
|
|
111
|
+
<PriorityBadge priority={milestone.priority} />
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<DetailHeader status={milestone.status} fields={fields} />
|
|
115
|
+
|
|
116
|
+
{milestone.notes && (
|
|
117
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">{milestone.notes}</p>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{/* Markdown content */}
|
|
121
|
+
{loading ? (
|
|
122
|
+
<p className="text-sm text-gray-600 dark:text-gray-500">Loading document...</p>
|
|
123
|
+
) : markdown ? (
|
|
124
|
+
<div className="prose-sm">
|
|
125
|
+
<MarkdownContent content={markdown} basePath={markdownFilePath ?? undefined} linkMap={linkMap} />
|
|
126
|
+
</div>
|
|
127
|
+
) : markdownError ? (
|
|
128
|
+
<div className="bg-gray-100 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800 rounded-xl p-4 text-sm text-gray-600 dark:text-gray-500">
|
|
129
|
+
No document found — {markdownError}
|
|
130
|
+
</div>
|
|
131
|
+
) : null}
|
|
132
|
+
|
|
133
|
+
{/* Task list */}
|
|
134
|
+
{tasks.length > 0 && (
|
|
135
|
+
<div className="mt-8">
|
|
136
|
+
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-400 uppercase tracking-wider mb-3">
|
|
137
|
+
Tasks
|
|
138
|
+
</h2>
|
|
139
|
+
<div className="bg-gray-100 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-800 rounded-xl divide-y divide-gray-200 dark:divide-gray-800">
|
|
140
|
+
{tasks.map((task) => (
|
|
141
|
+
<Link
|
|
142
|
+
key={task.id}
|
|
143
|
+
to="/tasks/$taskId"
|
|
144
|
+
params={{ taskId: task.id }}
|
|
145
|
+
className="flex items-center gap-2 px-4 py-2.5 text-sm hover:bg-gray-200/50 dark:hover:bg-gray-800/50 transition-colors first:rounded-t-xl last:rounded-b-xl"
|
|
146
|
+
>
|
|
147
|
+
<StatusDot status={task.status} />
|
|
148
|
+
<span className={task.status === 'completed' ? 'text-gray-500 dark:text-gray-500' : 'text-gray-900 dark:text-gray-200'}>
|
|
149
|
+
{task.name}
|
|
150
|
+
</span>
|
|
151
|
+
<PriorityBadge priority={task.priority} />
|
|
152
|
+
{task.estimated_hours && (
|
|
153
|
+
<span className="text-xs text-gray-600 dark:text-gray-600 ml-auto">{task.estimated_hours}h</span>
|
|
154
|
+
)}
|
|
155
|
+
</Link>
|
|
156
|
+
))}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
} from '@tanstack/react-table'
|
|
11
11
|
import { ChevronDown, ChevronRight, ArrowUpDown } from 'lucide-react'
|
|
12
12
|
import { StatusBadge } from './StatusBadge'
|
|
13
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
13
14
|
import { ProgressBar } from './ProgressBar'
|
|
15
|
+
import { PreviewButton } from './PreviewButton'
|
|
14
16
|
import { TaskList } from './TaskList'
|
|
15
17
|
import type { Milestone, Task } from '../lib/types'
|
|
16
18
|
|
|
@@ -53,14 +55,17 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
|
|
|
53
55
|
columnHelper.accessor('name', {
|
|
54
56
|
header: 'Milestone',
|
|
55
57
|
cell: (info) => (
|
|
56
|
-
<
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
<div className="flex items-center gap-2 group">
|
|
59
|
+
<Link
|
|
60
|
+
to="/milestones/$milestoneId"
|
|
61
|
+
params={{ milestoneId: info.row.original.id }}
|
|
62
|
+
className="text-sm font-medium text-gray-900 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
|
63
|
+
onClick={(e) => e.stopPropagation()}
|
|
64
|
+
>
|
|
65
|
+
{info.getValue()}
|
|
66
|
+
</Link>
|
|
67
|
+
<PreviewButton type="milestone" id={info.row.original.id} />
|
|
68
|
+
</div>
|
|
64
69
|
),
|
|
65
70
|
}),
|
|
66
71
|
columnHelper.accessor('status', {
|
|
@@ -68,6 +73,11 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
|
|
|
68
73
|
cell: (info) => <StatusBadge status={info.getValue()} />,
|
|
69
74
|
size: 120,
|
|
70
75
|
}),
|
|
76
|
+
columnHelper.accessor('priority', {
|
|
77
|
+
header: 'Priority',
|
|
78
|
+
cell: (info) => <PriorityBadge priority={info.getValue()} />,
|
|
79
|
+
size: 100,
|
|
80
|
+
}),
|
|
71
81
|
columnHelper.accessor('progress', {
|
|
72
82
|
header: 'Progress',
|
|
73
83
|
cell: (info) => (
|
|
@@ -2,7 +2,9 @@ import { useState } from 'react'
|
|
|
2
2
|
import { Link } from '@tanstack/react-router'
|
|
3
3
|
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
4
4
|
import { StatusBadge } from './StatusBadge'
|
|
5
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
5
6
|
import { ProgressBar } from './ProgressBar'
|
|
7
|
+
import { PreviewButton } from './PreviewButton'
|
|
6
8
|
import { TaskList } from './TaskList'
|
|
7
9
|
import { useCollapse } from '../lib/useCollapse'
|
|
8
10
|
import type { Milestone, Task } from '../lib/types'
|
|
@@ -26,29 +28,33 @@ function MilestoneTreeRow({
|
|
|
26
28
|
const collapse = useCollapse(expanded)
|
|
27
29
|
|
|
28
30
|
return (
|
|
29
|
-
<div className="border-b border-gray-800/50">
|
|
31
|
+
<div className="border-b border-gray-200 dark:border-gray-800/50 group">
|
|
30
32
|
<button
|
|
31
33
|
onClick={onToggle}
|
|
32
|
-
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-800/30 transition-colors text-left"
|
|
34
|
+
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-200/50 dark:hover:bg-gray-800/30 transition-colors text-left"
|
|
33
35
|
>
|
|
34
36
|
{expanded ? (
|
|
35
|
-
<ChevronDown className="w-4 h-4 text-gray-500 shrink-0" />
|
|
37
|
+
<ChevronDown className="w-4 h-4 text-gray-500 dark:text-gray-500 shrink-0" />
|
|
36
38
|
) : (
|
|
37
|
-
<ChevronRight className="w-4 h-4 text-gray-500 shrink-0" />
|
|
39
|
+
<ChevronRight className="w-4 h-4 text-gray-500 dark:text-gray-500 shrink-0" />
|
|
38
40
|
)}
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
<div className="flex items-center gap-2 flex-1">
|
|
42
|
+
<Link
|
|
43
|
+
to="/milestones/$milestoneId"
|
|
44
|
+
params={{ milestoneId: milestone.id }}
|
|
45
|
+
className="text-sm font-medium text-gray-900 dark:text-gray-200 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
|
|
46
|
+
onClick={(e) => e.stopPropagation()}
|
|
47
|
+
>
|
|
48
|
+
{milestone.name}
|
|
49
|
+
</Link>
|
|
50
|
+
<PreviewButton type="milestone" id={milestone.id} />
|
|
51
|
+
</div>
|
|
47
52
|
<StatusBadge status={milestone.status} />
|
|
53
|
+
<PriorityBadge priority={milestone.priority} />
|
|
48
54
|
<div className="w-20">
|
|
49
55
|
<ProgressBar value={milestone.progress} size="sm" />
|
|
50
56
|
</div>
|
|
51
|
-
<span className="text-xs text-gray-500 font-mono w-12 text-right">
|
|
57
|
+
<span className="text-xs text-gray-500 dark:text-gray-500 font-mono w-12 text-right">
|
|
52
58
|
{milestone.tasks_completed}/{milestone.tasks_total}
|
|
53
59
|
</span>
|
|
54
60
|
</button>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Eye } from 'lucide-react'
|
|
2
|
+
import { useSidePanel } from '../contexts/SidePanelContext'
|
|
3
|
+
|
|
4
|
+
interface PreviewButtonProps {
|
|
5
|
+
type: 'milestone' | 'task'
|
|
6
|
+
id: string
|
|
7
|
+
className?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PreviewButton({ type, id, className = '' }: PreviewButtonProps) {
|
|
11
|
+
const { openMilestone, openTask } = useSidePanel()
|
|
12
|
+
|
|
13
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
14
|
+
e.preventDefault()
|
|
15
|
+
e.stopPropagation()
|
|
16
|
+
if (type === 'milestone') {
|
|
17
|
+
openMilestone(id)
|
|
18
|
+
} else {
|
|
19
|
+
openTask(id)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
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}`}
|
|
27
|
+
title={`Preview ${type}`}
|
|
28
|
+
aria-label={`Preview ${type}`}
|
|
29
|
+
>
|
|
30
|
+
<Eye className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
|
31
|
+
</button>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Priority } from '../lib/types'
|
|
2
|
+
|
|
3
|
+
const priorityStyles: Record<Priority, string> = {
|
|
4
|
+
critical: 'bg-rose-500/20 text-rose-400',
|
|
5
|
+
high: 'bg-amber-500/20 text-amber-400',
|
|
6
|
+
medium: 'bg-blue-500/20 text-blue-400',
|
|
7
|
+
low: 'bg-zinc-500/20 text-zinc-400',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PriorityBadge({ priority }: { priority: Priority | undefined }) {
|
|
11
|
+
if (!priority) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<span
|
|
15
|
+
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium uppercase ${priorityStyles[priority] ?? priorityStyles.medium}`}
|
|
16
|
+
>
|
|
17
|
+
{priority}
|
|
18
|
+
</span>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { X } from 'lucide-react'
|
|
2
|
+
import { useSidePanel } from '../contexts/SidePanelContext'
|
|
3
|
+
import { MilestonePreview } from './MilestonePreview'
|
|
4
|
+
import { TaskPreview } from './TaskPreview'
|
|
5
|
+
|
|
6
|
+
export function SidePanel() {
|
|
7
|
+
const { content, isOpen, close } = useSidePanel()
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<>
|
|
11
|
+
{/* Backdrop */}
|
|
12
|
+
<div
|
|
13
|
+
className={`fixed inset-0 bg-black/30 backdrop-blur-sm z-40 transition-opacity duration-300 ${
|
|
14
|
+
isOpen ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
|
15
|
+
}`}
|
|
16
|
+
onClick={close}
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
{/* Panel */}
|
|
20
|
+
<div
|
|
21
|
+
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 ${
|
|
22
|
+
isOpen ? 'translate-x-0' : 'translate-x-full'
|
|
23
|
+
}`}
|
|
24
|
+
>
|
|
25
|
+
{/* Header */}
|
|
26
|
+
<div className="sticky top-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 px-6 py-4 flex items-center justify-between z-10">
|
|
27
|
+
<h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100">Preview</h2>
|
|
28
|
+
<button
|
|
29
|
+
onClick={close}
|
|
30
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
31
|
+
aria-label="Close panel"
|
|
32
|
+
>
|
|
33
|
+
<X className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
34
|
+
</button>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{/* Content */}
|
|
38
|
+
<div className="p-6">
|
|
39
|
+
{content?.type === 'milestone' && <MilestonePreview milestoneId={content.id} />}
|
|
40
|
+
{content?.type === 'task' && <TaskPreview taskId={content.id} />}
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
</>
|
|
44
|
+
)
|
|
45
|
+
}
|