@prmichaelsen/acp-visualizer 0.13.2 → 0.14.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 +1 -11
- package/src/components/DocumentList.tsx +17 -9
- package/src/components/DocumentPreview.tsx +123 -0
- package/src/components/FilterBar.tsx +2 -2
- package/src/components/MilestonePreview.tsx +1 -10
- package/src/components/PreviewButton.tsx +17 -11
- package/src/components/SearchInput.tsx +22 -3
- package/src/components/SidePanel.tsx +88 -32
- package/src/components/Sidebar.tsx +2 -1
- package/src/components/TaskPreview.tsx +1 -10
- package/src/contexts/SidePanelContext.tsx +8 -1
- package/src/lib/github-auth.ts +11 -0
- package/src/routeTree.gen.ts +71 -0
- package/src/routes/__root.tsx +10 -3
- package/src/routes/artifacts.$slug.tsx +18 -0
- package/src/routes/artifacts.index.tsx +10 -0
- package/src/routes/artifacts.tsx +9 -0
- package/src/routes/milestones.index.tsx +20 -18
package/package.json
CHANGED
|
@@ -3,6 +3,7 @@ import { Breadcrumb } from './Breadcrumb'
|
|
|
3
3
|
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
4
4
|
import { getMarkdownContent, listAgentDirectory } from '../services/markdown.service'
|
|
5
5
|
import { useProgressData } from '../contexts/ProgressContext'
|
|
6
|
+
import { getGitHubParams } from '../lib/github-auth'
|
|
6
7
|
import type { MarkdownResult, AgentFile } from '../services/markdown.service'
|
|
7
8
|
|
|
8
9
|
interface DocumentDetailProps {
|
|
@@ -12,17 +13,6 @@ interface DocumentDetailProps {
|
|
|
12
13
|
sectionHref: string
|
|
13
14
|
}
|
|
14
15
|
|
|
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
16
|
export function DocumentDetail({ slug, dirPath, sectionLabel, sectionHref }: DocumentDetailProps) {
|
|
27
17
|
const data = useProgressData()
|
|
28
18
|
const [markdown, setMarkdown] = useState<string | null>(null)
|
|
@@ -2,6 +2,8 @@ import { Link } from '@tanstack/react-router'
|
|
|
2
2
|
import { useState, useEffect } from 'react'
|
|
3
3
|
import { listAgentDirectory } from '../services/markdown.service'
|
|
4
4
|
import type { AgentFile } from '../services/markdown.service'
|
|
5
|
+
import { getGitHubParams } from '../lib/github-auth'
|
|
6
|
+
import { PreviewButton } from './PreviewButton'
|
|
5
7
|
import { FileText } from 'lucide-react'
|
|
6
8
|
|
|
7
9
|
interface DocumentListProps {
|
|
@@ -11,13 +13,14 @@ interface DocumentListProps {
|
|
|
11
13
|
github?: { owner: string; repo: string; branch?: string; token?: string }
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
export function DocumentList({ title, dirPath, baseTo, github }: DocumentListProps) {
|
|
16
|
+
export function DocumentList({ title, dirPath, baseTo, github: githubProp }: DocumentListProps) {
|
|
15
17
|
const [files, setFiles] = useState<AgentFile[]>([])
|
|
16
18
|
const [error, setError] = useState<string | null>(null)
|
|
17
19
|
const [loading, setLoading] = useState(true)
|
|
18
20
|
const [directoryExists, setDirectoryExists] = useState(true)
|
|
19
21
|
|
|
20
22
|
useEffect(() => {
|
|
23
|
+
const github = githubProp ?? getGitHubParams()
|
|
21
24
|
setLoading(true)
|
|
22
25
|
setDirectoryExists(true)
|
|
23
26
|
listAgentDirectory({ data: { dirPath, github } })
|
|
@@ -31,7 +34,7 @@ export function DocumentList({ title, dirPath, baseTo, github }: DocumentListPro
|
|
|
31
34
|
})
|
|
32
35
|
.catch((err: Error) => setError(err.message))
|
|
33
36
|
.finally(() => setLoading(false))
|
|
34
|
-
}, [dirPath,
|
|
37
|
+
}, [dirPath, githubProp])
|
|
35
38
|
|
|
36
39
|
if (loading) {
|
|
37
40
|
return (
|
|
@@ -57,7 +60,7 @@ export function DocumentList({ title, dirPath, baseTo, github }: DocumentListPro
|
|
|
57
60
|
return (
|
|
58
61
|
<div className="p-6">
|
|
59
62
|
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
60
|
-
{!directoryExists &&
|
|
63
|
+
{!directoryExists && (githubProp || getGitHubParams()) ? (
|
|
61
64
|
<div className="bg-yellow-900/20 border border-yellow-800/30 rounded-lg p-4">
|
|
62
65
|
<p className="text-sm text-yellow-300 mb-2">
|
|
63
66
|
Directory <code className="text-yellow-400 bg-yellow-900/30 px-1.5 py-0.5 rounded">{dirPath}/</code> not found in this repository.
|
|
@@ -78,15 +81,20 @@ export function DocumentList({ title, dirPath, baseTo, github }: DocumentListPro
|
|
|
78
81
|
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
79
82
|
<div className="bg-gray-900/50 border border-gray-800 rounded-xl divide-y divide-gray-800">
|
|
80
83
|
{files.map((file) => (
|
|
81
|
-
<
|
|
84
|
+
<div
|
|
82
85
|
key={file.name}
|
|
83
|
-
to={baseTo + '/$slug'}
|
|
84
|
-
params={{ slug: file.name }}
|
|
85
86
|
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"
|
|
86
87
|
>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
<Link
|
|
89
|
+
to={baseTo + '/$slug'}
|
|
90
|
+
params={{ slug: file.name }}
|
|
91
|
+
className="flex items-center gap-3 flex-1 min-w-0"
|
|
92
|
+
>
|
|
93
|
+
<FileText className="w-4 h-4 text-gray-500 shrink-0" />
|
|
94
|
+
<span className="text-gray-200">{formatName(file.name)}</span>
|
|
95
|
+
</Link>
|
|
96
|
+
<PreviewButton type="document" dirPath={dirPath} slug={file.name} />
|
|
97
|
+
</div>
|
|
90
98
|
))}
|
|
91
99
|
</div>
|
|
92
100
|
<p className="text-xs text-gray-600 mt-3">{files.length} document{files.length !== 1 ? 's' : ''}</p>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
3
|
+
import { Maximize2 } from 'lucide-react'
|
|
4
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
5
|
+
import { useSidePanel } from '../contexts/SidePanelContext'
|
|
6
|
+
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
7
|
+
import { getMarkdownContent, listAgentDirectory } from '../services/markdown.service'
|
|
8
|
+
import { getGitHubParams } from '../lib/github-auth'
|
|
9
|
+
import type { MarkdownResult, AgentFile } from '../services/markdown.service'
|
|
10
|
+
|
|
11
|
+
interface DocumentPreviewProps {
|
|
12
|
+
dirPath: string
|
|
13
|
+
slug: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Map dirPath to route base path */
|
|
17
|
+
function dirToRoute(dirPath: string): string {
|
|
18
|
+
if (dirPath.includes('design')) return '/designs'
|
|
19
|
+
if (dirPath.includes('pattern')) return '/patterns'
|
|
20
|
+
if (dirPath.includes('artifact')) return '/artifacts'
|
|
21
|
+
return '/designs'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Map dirPath to section label */
|
|
25
|
+
function dirToLabel(dirPath: string): string {
|
|
26
|
+
if (dirPath.includes('design')) return 'Design'
|
|
27
|
+
if (dirPath.includes('pattern')) return 'Pattern'
|
|
28
|
+
if (dirPath.includes('artifact')) return 'Artifact'
|
|
29
|
+
return 'Document'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Turn "local.dashboard-layout-routing" into "Dashboard Layout Routing" */
|
|
33
|
+
function formatName(name: string): string {
|
|
34
|
+
const stripped = name.replace(/^[a-z0-9-]+\./, '')
|
|
35
|
+
return stripped
|
|
36
|
+
.replace(/[-_]/g, ' ')
|
|
37
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function DocumentPreview({ dirPath, slug }: DocumentPreviewProps) {
|
|
41
|
+
const data = useProgressData()
|
|
42
|
+
const { close } = useSidePanel()
|
|
43
|
+
const [markdown, setMarkdown] = useState<string | null>(null)
|
|
44
|
+
const [markdownError, setMarkdownError] = useState<string | null>(null)
|
|
45
|
+
const [filePath, setFilePath] = useState<string | null>(null)
|
|
46
|
+
const [loading, setLoading] = useState(true)
|
|
47
|
+
const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
|
|
48
|
+
|
|
49
|
+
const routeBase = dirToRoute(dirPath)
|
|
50
|
+
const sectionLabel = dirToLabel(dirPath)
|
|
51
|
+
const displayName = formatName(slug)
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
setLoading(true)
|
|
55
|
+
setMarkdown(null)
|
|
56
|
+
setMarkdownError(null)
|
|
57
|
+
setFilePath(null)
|
|
58
|
+
|
|
59
|
+
const github = getGitHubParams()
|
|
60
|
+
const relativePath = `${dirPath}/${slug}.md`
|
|
61
|
+
|
|
62
|
+
getMarkdownContent({ data: { filePath: relativePath, github } })
|
|
63
|
+
.then((result: MarkdownResult) => {
|
|
64
|
+
if (result.ok) {
|
|
65
|
+
setMarkdown(result.content)
|
|
66
|
+
setFilePath(result.filePath)
|
|
67
|
+
} else {
|
|
68
|
+
// Fallback: search directory for a file containing the slug
|
|
69
|
+
return listAgentDirectory({ data: { dirPath, github } }).then((listResult) => {
|
|
70
|
+
if (listResult.ok) {
|
|
71
|
+
const match = listResult.files.find((f: AgentFile) => f.name === slug)
|
|
72
|
+
if (match) {
|
|
73
|
+
return getMarkdownContent({ data: { filePath: match.relativePath, github } })
|
|
74
|
+
.then((mdResult: MarkdownResult) => {
|
|
75
|
+
if (mdResult.ok) {
|
|
76
|
+
setMarkdown(mdResult.content)
|
|
77
|
+
setFilePath(mdResult.filePath)
|
|
78
|
+
} else {
|
|
79
|
+
setMarkdownError(mdResult.error)
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
setMarkdownError(result.error)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.catch((err: Error) => setMarkdownError(err.message))
|
|
89
|
+
.finally(() => setLoading(false))
|
|
90
|
+
}, [dirPath, slug])
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div>
|
|
94
|
+
<div className="flex items-start justify-between mb-4">
|
|
95
|
+
<div>
|
|
96
|
+
<p className="text-xs text-gray-500 dark:text-gray-500 uppercase tracking-wider mb-1">{sectionLabel}</p>
|
|
97
|
+
<h1 className="text-lg font-semibold text-gray-900 dark:text-gray-100">{displayName}</h1>
|
|
98
|
+
</div>
|
|
99
|
+
<Link
|
|
100
|
+
to={routeBase + '/$slug'}
|
|
101
|
+
params={{ slug }}
|
|
102
|
+
className="p-1.5 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
103
|
+
title="Open full view"
|
|
104
|
+
onClick={close}
|
|
105
|
+
>
|
|
106
|
+
<Maximize2 className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
107
|
+
</Link>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{loading ? (
|
|
111
|
+
<p className="text-sm text-gray-600 dark:text-gray-500">Loading document...</p>
|
|
112
|
+
) : markdown ? (
|
|
113
|
+
<div className="prose-sm">
|
|
114
|
+
<MarkdownContent content={markdown} basePath={filePath ?? undefined} linkMap={linkMap} />
|
|
115
|
+
</div>
|
|
116
|
+
) : markdownError ? (
|
|
117
|
+
<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">
|
|
118
|
+
No document found — {markdownError}
|
|
119
|
+
</div>
|
|
120
|
+
) : null}
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -15,12 +15,12 @@ interface FilterBarProps {
|
|
|
15
15
|
|
|
16
16
|
export function FilterBar({ status, onStatusChange }: FilterBarProps) {
|
|
17
17
|
return (
|
|
18
|
-
<div className="flex
|
|
18
|
+
<div className="flex gap-1 p-1 bg-gray-800/50 rounded-lg overflow-x-auto scrollbar-hide">
|
|
19
19
|
{statusOptions.map((opt) => (
|
|
20
20
|
<button
|
|
21
21
|
key={opt.value}
|
|
22
22
|
onClick={() => onStatusChange(opt.value)}
|
|
23
|
-
className={`px-
|
|
23
|
+
className={`flex-shrink-0 px-3 py-1.5 text-sm font-medium rounded-md transition-all whitespace-nowrap min-w-[44px] min-h-[44px] flex items-center justify-center ${
|
|
24
24
|
status === opt.value
|
|
25
25
|
? 'bg-gray-700 text-gray-100 shadow-sm'
|
|
26
26
|
: 'text-gray-500 hover:text-gray-300'
|
|
@@ -9,6 +9,7 @@ import { StatusDot } from './StatusDot'
|
|
|
9
9
|
import { PriorityBadge } from './PriorityBadge'
|
|
10
10
|
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
11
11
|
import { getMarkdownContent, resolveMilestoneFile } from '../services/markdown.service'
|
|
12
|
+
import { getGitHubParams } from '../lib/github-auth'
|
|
12
13
|
import { formatMilestoneName } from '../lib/display'
|
|
13
14
|
import type { MarkdownResult, ResolveFileResult } from '../services/markdown.service'
|
|
14
15
|
|
|
@@ -16,16 +17,6 @@ interface MilestonePreviewProps {
|
|
|
16
17
|
milestoneId: string
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function getGitHubParams(): { owner: string; repo: string } | undefined {
|
|
20
|
-
if (typeof window === 'undefined') return undefined
|
|
21
|
-
const params = new URLSearchParams(window.location.search)
|
|
22
|
-
const repo = params.get('repo')
|
|
23
|
-
if (!repo) return undefined
|
|
24
|
-
const parts = repo.split('/')
|
|
25
|
-
if (parts.length < 2) return undefined
|
|
26
|
-
return { owner: parts[0], repo: parts[1] }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
export function MilestonePreview({ milestoneId }: MilestonePreviewProps) {
|
|
30
21
|
const data = useProgressData()
|
|
31
22
|
const { close } = useSidePanel()
|
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
import { PanelRight } from 'lucide-react'
|
|
2
2
|
import { useSidePanel } from '../contexts/SidePanelContext'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
type: 'milestone' | 'task'
|
|
6
|
-
id: string
|
|
4
|
+
type PreviewButtonProps = {
|
|
7
5
|
className?: string
|
|
8
|
-
}
|
|
6
|
+
} & (
|
|
7
|
+
| { type: 'milestone' | 'task'; id: string }
|
|
8
|
+
| { type: 'document'; dirPath: string; slug: string }
|
|
9
|
+
)
|
|
9
10
|
|
|
10
|
-
export function PreviewButton(
|
|
11
|
-
const {
|
|
11
|
+
export function PreviewButton(props: PreviewButtonProps) {
|
|
12
|
+
const { className = '' } = props
|
|
13
|
+
const { openMilestone, openTask, openDocument } = useSidePanel()
|
|
12
14
|
|
|
13
15
|
const handleClick = (e: React.MouseEvent) => {
|
|
14
16
|
e.preventDefault()
|
|
15
17
|
e.stopPropagation()
|
|
16
|
-
if (type === 'milestone') {
|
|
17
|
-
openMilestone(id)
|
|
18
|
+
if (props.type === 'milestone') {
|
|
19
|
+
openMilestone(props.id)
|
|
20
|
+
} else if (props.type === 'task') {
|
|
21
|
+
openTask(props.id)
|
|
18
22
|
} else {
|
|
19
|
-
|
|
23
|
+
openDocument(props.dirPath, props.slug)
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
|
|
27
|
+
const label = props.type === 'document' ? 'document' : props.type
|
|
28
|
+
|
|
23
29
|
return (
|
|
24
30
|
<button
|
|
25
31
|
onClick={handleClick}
|
|
26
32
|
className={`p-1.5 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-800 transition-colors ${className}`}
|
|
27
|
-
title={`Preview ${
|
|
28
|
-
aria-label={`Preview ${
|
|
33
|
+
title={`Preview ${label}`}
|
|
34
|
+
aria-label={`Preview ${label}`}
|
|
29
35
|
>
|
|
30
36
|
<PanelRight className="w-3.5 h-3.5 text-gray-500 dark:text-gray-400" />
|
|
31
37
|
</button>
|
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
import { Search } from 'lucide-react'
|
|
2
|
+
import { useState, useEffect, useRef } from 'react'
|
|
2
3
|
|
|
3
4
|
interface SearchInputProps {
|
|
4
5
|
value: string
|
|
5
6
|
onChange: (value: string) => void
|
|
6
7
|
placeholder?: string
|
|
8
|
+
debounceMs?: number
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) {
|
|
11
|
+
export function SearchInput({ value, onChange, placeholder = 'Search...', debounceMs = 250 }: SearchInputProps) {
|
|
12
|
+
const [localValue, setLocalValue] = useState(value)
|
|
13
|
+
const timerRef = useRef<ReturnType<typeof setTimeout>>(null)
|
|
14
|
+
|
|
15
|
+
// Sync local value when external value changes (e.g. URL navigation)
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
setLocalValue(value)
|
|
18
|
+
}, [value])
|
|
19
|
+
|
|
20
|
+
const handleChange = (next: string) => {
|
|
21
|
+
setLocalValue(next)
|
|
22
|
+
if (timerRef.current) clearTimeout(timerRef.current)
|
|
23
|
+
timerRef.current = setTimeout(() => onChange(next), debounceMs)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Cleanup timer on unmount
|
|
27
|
+
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current) }, [])
|
|
28
|
+
|
|
10
29
|
return (
|
|
11
30
|
<div className="relative">
|
|
12
31
|
<Search className="absolute left-3 top-2.5 w-4 h-4 text-gray-500" />
|
|
13
32
|
<input
|
|
14
33
|
type="text"
|
|
15
|
-
value={
|
|
16
|
-
onChange={(e) =>
|
|
34
|
+
value={localValue}
|
|
35
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
17
36
|
placeholder={placeholder}
|
|
18
37
|
className="w-full bg-gray-900 border border-gray-800 rounded-md pl-10 pr-3 py-2 text-base text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-600 transition-colors"
|
|
19
38
|
/>
|
|
@@ -1,13 +1,30 @@
|
|
|
1
1
|
import { X } from 'lucide-react'
|
|
2
|
-
import { useRef, useState, useEffect } from 'react'
|
|
2
|
+
import { useRef, useState, useEffect, useCallback } from 'react'
|
|
3
3
|
import { useSidePanel } from '../contexts/SidePanelContext'
|
|
4
4
|
import { MilestonePreview } from './MilestonePreview'
|
|
5
5
|
import { TaskPreview } from './TaskPreview'
|
|
6
|
+
import { DocumentPreview } from './DocumentPreview'
|
|
7
|
+
|
|
8
|
+
function useIsDesktop() {
|
|
9
|
+
const [isDesktop, setIsDesktop] = useState(() =>
|
|
10
|
+
typeof window !== 'undefined' ? window.matchMedia('(min-width: 1024px)').matches : true
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const mql = window.matchMedia('(min-width: 1024px)')
|
|
15
|
+
const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches)
|
|
16
|
+
mql.addEventListener('change', handler)
|
|
17
|
+
return () => mql.removeEventListener('change', handler)
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
return isDesktop
|
|
21
|
+
}
|
|
6
22
|
|
|
7
23
|
export function SidePanel() {
|
|
8
24
|
const { content, isOpen, width, close, setWidth } = useSidePanel()
|
|
9
25
|
const [isResizing, setIsResizing] = useState(false)
|
|
10
26
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
27
|
+
const isDesktop = useIsDesktop()
|
|
11
28
|
|
|
12
29
|
// Handle resize drag
|
|
13
30
|
useEffect(() => {
|
|
@@ -44,45 +61,84 @@ export function SidePanel() {
|
|
|
44
61
|
}
|
|
45
62
|
}, [isResizing])
|
|
46
63
|
|
|
64
|
+
// Lock body scroll when bottom sheet is open on mobile
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!isOpen || isDesktop) return
|
|
67
|
+
document.body.style.overflow = 'hidden'
|
|
68
|
+
return () => { document.body.style.overflow = '' }
|
|
69
|
+
}, [isOpen, isDesktop])
|
|
70
|
+
|
|
47
71
|
// Don't render at all until first open
|
|
48
72
|
if (!content && !isOpen) {
|
|
49
73
|
return null
|
|
50
74
|
}
|
|
51
75
|
|
|
76
|
+
// Desktop: inline width for resizable side panel
|
|
77
|
+
// Mobile: no inline width, full-width via fixed positioning
|
|
78
|
+
const panelStyle = isDesktop
|
|
79
|
+
? { width: isOpen ? `${width}px` : '0px' }
|
|
80
|
+
: undefined
|
|
81
|
+
|
|
52
82
|
return (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
style={{ width: isOpen ? `${width}px` : '0px' }}
|
|
59
|
-
>
|
|
60
|
-
{/* Resize Handle */}
|
|
61
|
-
<div
|
|
62
|
-
className={`absolute top-0 left-0 bottom-0 w-1 cursor-ew-resize hover:bg-blue-500 hover:w-1 transition-all z-20 ${
|
|
63
|
-
isResizing ? 'bg-blue-500 w-1' : 'bg-transparent'
|
|
64
|
-
}`}
|
|
65
|
-
onMouseDown={() => setIsResizing(true)}
|
|
66
|
-
aria-label="Resize panel"
|
|
67
|
-
/>
|
|
68
|
-
|
|
69
|
-
{/* Header */}
|
|
70
|
-
<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">
|
|
71
|
-
<h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100">Preview</h2>
|
|
72
|
-
<button
|
|
83
|
+
<>
|
|
84
|
+
{/* Mobile backdrop */}
|
|
85
|
+
{isOpen && !isDesktop && (
|
|
86
|
+
<div
|
|
87
|
+
className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40"
|
|
73
88
|
onClick={close}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
/>
|
|
90
|
+
)}
|
|
91
|
+
|
|
92
|
+
{/* Desktop: side panel | Mobile: bottom sheet */}
|
|
93
|
+
<div
|
|
94
|
+
ref={panelRef}
|
|
95
|
+
className={[
|
|
96
|
+
// Base
|
|
97
|
+
'bg-white dark:bg-gray-900 shadow-lg overflow-hidden transition-all duration-300 flex-shrink-0',
|
|
98
|
+
// Mobile: bottom sheet
|
|
99
|
+
'fixed bottom-0 left-0 right-0 z-50 rounded-t-2xl max-h-[85vh] border-t border-gray-200 dark:border-gray-800',
|
|
100
|
+
// Desktop: side panel
|
|
101
|
+
'lg:relative lg:bottom-auto lg:left-auto lg:right-auto lg:z-auto lg:rounded-none lg:max-h-none lg:h-full lg:border-t-0 lg:border-l',
|
|
102
|
+
// Open/close states
|
|
103
|
+
isOpen
|
|
104
|
+
? 'opacity-100 translate-y-0'
|
|
105
|
+
: 'opacity-0 translate-y-full lg:translate-y-0 lg:w-0 lg:border-l-0',
|
|
106
|
+
].join(' ')}
|
|
107
|
+
style={panelStyle}
|
|
108
|
+
>
|
|
109
|
+
{/* Resize Handle — desktop only */}
|
|
110
|
+
<div
|
|
111
|
+
className={`hidden lg:block absolute top-0 left-0 bottom-0 w-1 cursor-ew-resize hover:bg-blue-500 hover:w-1 transition-all z-20 ${
|
|
112
|
+
isResizing ? 'bg-blue-500 w-1' : 'bg-transparent'
|
|
113
|
+
}`}
|
|
114
|
+
onMouseDown={() => setIsResizing(true)}
|
|
115
|
+
aria-label="Resize panel"
|
|
116
|
+
/>
|
|
117
|
+
|
|
118
|
+
{/* Drag indicator — mobile only */}
|
|
119
|
+
<div className="lg:hidden flex justify-center pt-2 pb-1">
|
|
120
|
+
<div className="w-10 h-1 rounded-full bg-gray-300 dark:bg-gray-600" />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
{/* Header */}
|
|
124
|
+
<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">
|
|
125
|
+
<h2 className="text-sm font-semibold text-gray-900 dark:text-gray-100">Preview</h2>
|
|
126
|
+
<button
|
|
127
|
+
onClick={close}
|
|
128
|
+
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
129
|
+
aria-label="Close panel"
|
|
130
|
+
>
|
|
131
|
+
<X className="w-4 h-4 text-gray-600 dark:text-gray-400" />
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
80
134
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
135
|
+
{/* Content */}
|
|
136
|
+
<div className="p-6 overflow-auto h-[60vh] lg:h-[calc(100%-57px)]">
|
|
137
|
+
{content?.type === 'milestone' && <MilestonePreview milestoneId={content.id} />}
|
|
138
|
+
{content?.type === 'task' && <TaskPreview taskId={content.id} />}
|
|
139
|
+
{content?.type === 'document' && <DocumentPreview dirPath={content.dirPath} slug={content.slug} />}
|
|
140
|
+
</div>
|
|
85
141
|
</div>
|
|
86
|
-
|
|
142
|
+
</>
|
|
87
143
|
)
|
|
88
144
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Link, useRouterState } from '@tanstack/react-router'
|
|
2
|
-
import { LayoutDashboard, Flag, CheckSquare, Clock, Search, PenTool, Puzzle, FileBarChart, Github, X, ChevronLeft, ChevronRight } from 'lucide-react'
|
|
2
|
+
import { LayoutDashboard, Flag, CheckSquare, Clock, Search, PenTool, Puzzle, Archive, FileBarChart, Github, X, ChevronLeft, ChevronRight } from 'lucide-react'
|
|
3
3
|
import { ProjectSelector } from './ProjectSelector'
|
|
4
4
|
import { GitHubInput } from './GitHubInput'
|
|
5
5
|
import { GitHubAuth } from './GitHubAuth'
|
|
@@ -12,6 +12,7 @@ const navItems = [
|
|
|
12
12
|
{ to: '/activity' as const, icon: Clock, label: 'Activity' },
|
|
13
13
|
{ to: '/designs' as const, icon: PenTool, label: 'Designs' },
|
|
14
14
|
{ to: '/patterns' as const, icon: Puzzle, label: 'Patterns' },
|
|
15
|
+
{ to: '/artifacts' as const, icon: Archive, label: 'Artifacts' },
|
|
15
16
|
{ to: '/reports' as const, icon: FileBarChart, label: 'Reports' },
|
|
16
17
|
{ to: '/github' as const, icon: Github, label: 'GitHub' },
|
|
17
18
|
]
|
|
@@ -7,6 +7,7 @@ import { DetailHeader } from './DetailHeader'
|
|
|
7
7
|
import { PriorityBadge } from './PriorityBadge'
|
|
8
8
|
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
9
9
|
import { getMarkdownContent, resolveTaskFile } from '../services/markdown.service'
|
|
10
|
+
import { getGitHubParams } from '../lib/github-auth'
|
|
10
11
|
import { formatTaskName, formatMilestoneName } from '../lib/display'
|
|
11
12
|
import type { MarkdownResult } from '../services/markdown.service'
|
|
12
13
|
|
|
@@ -14,16 +15,6 @@ interface TaskPreviewProps {
|
|
|
14
15
|
taskId: string
|
|
15
16
|
}
|
|
16
17
|
|
|
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
18
|
export function TaskPreview({ taskId }: TaskPreviewProps) {
|
|
28
19
|
const data = useProgressData()
|
|
29
20
|
const { close } = useSidePanel()
|
|
@@ -3,6 +3,7 @@ import { createContext, useContext, useState, ReactNode } from 'react'
|
|
|
3
3
|
type PanelContent =
|
|
4
4
|
| { type: 'milestone'; id: string }
|
|
5
5
|
| { type: 'task'; id: string }
|
|
6
|
+
| { type: 'document'; dirPath: string; slug: string }
|
|
6
7
|
| null
|
|
7
8
|
|
|
8
9
|
interface SidePanelContextValue {
|
|
@@ -11,6 +12,7 @@ interface SidePanelContextValue {
|
|
|
11
12
|
width: number
|
|
12
13
|
openMilestone: (id: string) => void
|
|
13
14
|
openTask: (id: string) => void
|
|
15
|
+
openDocument: (dirPath: string, slug: string) => void
|
|
14
16
|
close: () => void
|
|
15
17
|
setWidth: (width: number) => void
|
|
16
18
|
}
|
|
@@ -46,6 +48,11 @@ export function SidePanelProvider({ children }: { children: ReactNode }) {
|
|
|
46
48
|
setIsOpen(true)
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
const openDocument = (dirPath: string, slug: string) => {
|
|
52
|
+
setContent({ type: 'document', dirPath, slug })
|
|
53
|
+
setIsOpen(true)
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
const close = () => {
|
|
50
57
|
setIsOpen(false)
|
|
51
58
|
setTimeout(() => setContent(null), 300) // Wait for animation
|
|
@@ -58,7 +65,7 @@ export function SidePanelProvider({ children }: { children: ReactNode }) {
|
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
return (
|
|
61
|
-
<SidePanelContext.Provider value={{ content, isOpen, width, openMilestone, openTask, close, setWidth }}>
|
|
68
|
+
<SidePanelContext.Provider value={{ content, isOpen, width, openMilestone, openTask, openDocument, close, setWidth }}>
|
|
62
69
|
{children}
|
|
63
70
|
</SidePanelContext.Provider>
|
|
64
71
|
)
|
package/src/lib/github-auth.ts
CHANGED
|
@@ -39,6 +39,17 @@ export function setStoredUser(user: GitHubUser) {
|
|
|
39
39
|
localStorage.setItem(USER_KEY, JSON.stringify(user))
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/** Read ?repo=owner/repo from URL search params */
|
|
43
|
+
export function getGitHubParams(): { owner: string; repo: string } | undefined {
|
|
44
|
+
if (typeof window === 'undefined') return undefined
|
|
45
|
+
const params = new URLSearchParams(window.location.search)
|
|
46
|
+
const repo = params.get('repo')
|
|
47
|
+
if (!repo) return undefined
|
|
48
|
+
const parts = repo.split('/')
|
|
49
|
+
if (parts.length < 2) return undefined
|
|
50
|
+
return { owner: parts[0], repo: parts[1] }
|
|
51
|
+
}
|
|
52
|
+
|
|
42
53
|
export function getGitHubAuthUrl(clientId: string, redirectUri: string): string {
|
|
43
54
|
const state = crypto.randomUUID()
|
|
44
55
|
sessionStorage.setItem('github_oauth_state', state)
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Route as PatternsRouteImport } from './routes/patterns'
|
|
|
16
16
|
import { Route as MilestonesRouteImport } from './routes/milestones'
|
|
17
17
|
import { Route as GithubRouteImport } from './routes/github'
|
|
18
18
|
import { Route as DesignsRouteImport } from './routes/designs'
|
|
19
|
+
import { Route as ArtifactsRouteImport } from './routes/artifacts'
|
|
19
20
|
import { Route as ActivityRouteImport } from './routes/activity'
|
|
20
21
|
import { Route as IndexRouteImport } from './routes/index'
|
|
21
22
|
import { Route as TasksIndexRouteImport } from './routes/tasks.index'
|
|
@@ -23,11 +24,13 @@ import { Route as ReportsIndexRouteImport } from './routes/reports.index'
|
|
|
23
24
|
import { Route as PatternsIndexRouteImport } from './routes/patterns.index'
|
|
24
25
|
import { Route as MilestonesIndexRouteImport } from './routes/milestones.index'
|
|
25
26
|
import { Route as DesignsIndexRouteImport } from './routes/designs.index'
|
|
27
|
+
import { Route as ArtifactsIndexRouteImport } from './routes/artifacts.index'
|
|
26
28
|
import { Route as TasksTaskIdRouteImport } from './routes/tasks.$taskId'
|
|
27
29
|
import { Route as ReportsSlugRouteImport } from './routes/reports.$slug'
|
|
28
30
|
import { Route as PatternsSlugRouteImport } from './routes/patterns.$slug'
|
|
29
31
|
import { Route as MilestonesMilestoneIdRouteImport } from './routes/milestones.$milestoneId'
|
|
30
32
|
import { Route as DesignsSlugRouteImport } from './routes/designs.$slug'
|
|
33
|
+
import { Route as ArtifactsSlugRouteImport } from './routes/artifacts.$slug'
|
|
31
34
|
import { Route as ApiWatchRouteImport } from './routes/api/watch'
|
|
32
35
|
import { Route as AuthGithubCallbackRouteImport } from './routes/auth/github/callback'
|
|
33
36
|
|
|
@@ -66,6 +69,11 @@ const DesignsRoute = DesignsRouteImport.update({
|
|
|
66
69
|
path: '/designs',
|
|
67
70
|
getParentRoute: () => rootRouteImport,
|
|
68
71
|
} as any)
|
|
72
|
+
const ArtifactsRoute = ArtifactsRouteImport.update({
|
|
73
|
+
id: '/artifacts',
|
|
74
|
+
path: '/artifacts',
|
|
75
|
+
getParentRoute: () => rootRouteImport,
|
|
76
|
+
} as any)
|
|
69
77
|
const ActivityRoute = ActivityRouteImport.update({
|
|
70
78
|
id: '/activity',
|
|
71
79
|
path: '/activity',
|
|
@@ -101,6 +109,11 @@ const DesignsIndexRoute = DesignsIndexRouteImport.update({
|
|
|
101
109
|
path: '/',
|
|
102
110
|
getParentRoute: () => DesignsRoute,
|
|
103
111
|
} as any)
|
|
112
|
+
const ArtifactsIndexRoute = ArtifactsIndexRouteImport.update({
|
|
113
|
+
id: '/',
|
|
114
|
+
path: '/',
|
|
115
|
+
getParentRoute: () => ArtifactsRoute,
|
|
116
|
+
} as any)
|
|
104
117
|
const TasksTaskIdRoute = TasksTaskIdRouteImport.update({
|
|
105
118
|
id: '/$taskId',
|
|
106
119
|
path: '/$taskId',
|
|
@@ -126,6 +139,11 @@ const DesignsSlugRoute = DesignsSlugRouteImport.update({
|
|
|
126
139
|
path: '/$slug',
|
|
127
140
|
getParentRoute: () => DesignsRoute,
|
|
128
141
|
} as any)
|
|
142
|
+
const ArtifactsSlugRoute = ArtifactsSlugRouteImport.update({
|
|
143
|
+
id: '/$slug',
|
|
144
|
+
path: '/$slug',
|
|
145
|
+
getParentRoute: () => ArtifactsRoute,
|
|
146
|
+
} as any)
|
|
129
147
|
const ApiWatchRoute = ApiWatchRouteImport.update({
|
|
130
148
|
id: '/api/watch',
|
|
131
149
|
path: '/api/watch',
|
|
@@ -140,6 +158,7 @@ const AuthGithubCallbackRoute = AuthGithubCallbackRouteImport.update({
|
|
|
140
158
|
export interface FileRoutesByFullPath {
|
|
141
159
|
'/': typeof IndexRoute
|
|
142
160
|
'/activity': typeof ActivityRoute
|
|
161
|
+
'/artifacts': typeof ArtifactsRouteWithChildren
|
|
143
162
|
'/designs': typeof DesignsRouteWithChildren
|
|
144
163
|
'/github': typeof GithubRoute
|
|
145
164
|
'/milestones': typeof MilestonesRouteWithChildren
|
|
@@ -148,11 +167,13 @@ export interface FileRoutesByFullPath {
|
|
|
148
167
|
'/search': typeof SearchRoute
|
|
149
168
|
'/tasks': typeof TasksRouteWithChildren
|
|
150
169
|
'/api/watch': typeof ApiWatchRoute
|
|
170
|
+
'/artifacts/$slug': typeof ArtifactsSlugRoute
|
|
151
171
|
'/designs/$slug': typeof DesignsSlugRoute
|
|
152
172
|
'/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
|
|
153
173
|
'/patterns/$slug': typeof PatternsSlugRoute
|
|
154
174
|
'/reports/$slug': typeof ReportsSlugRoute
|
|
155
175
|
'/tasks/$taskId': typeof TasksTaskIdRoute
|
|
176
|
+
'/artifacts/': typeof ArtifactsIndexRoute
|
|
156
177
|
'/designs/': typeof DesignsIndexRoute
|
|
157
178
|
'/milestones/': typeof MilestonesIndexRoute
|
|
158
179
|
'/patterns/': typeof PatternsIndexRoute
|
|
@@ -166,11 +187,13 @@ export interface FileRoutesByTo {
|
|
|
166
187
|
'/github': typeof GithubRoute
|
|
167
188
|
'/search': typeof SearchRoute
|
|
168
189
|
'/api/watch': typeof ApiWatchRoute
|
|
190
|
+
'/artifacts/$slug': typeof ArtifactsSlugRoute
|
|
169
191
|
'/designs/$slug': typeof DesignsSlugRoute
|
|
170
192
|
'/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
|
|
171
193
|
'/patterns/$slug': typeof PatternsSlugRoute
|
|
172
194
|
'/reports/$slug': typeof ReportsSlugRoute
|
|
173
195
|
'/tasks/$taskId': typeof TasksTaskIdRoute
|
|
196
|
+
'/artifacts': typeof ArtifactsIndexRoute
|
|
174
197
|
'/designs': typeof DesignsIndexRoute
|
|
175
198
|
'/milestones': typeof MilestonesIndexRoute
|
|
176
199
|
'/patterns': typeof PatternsIndexRoute
|
|
@@ -182,6 +205,7 @@ export interface FileRoutesById {
|
|
|
182
205
|
__root__: typeof rootRouteImport
|
|
183
206
|
'/': typeof IndexRoute
|
|
184
207
|
'/activity': typeof ActivityRoute
|
|
208
|
+
'/artifacts': typeof ArtifactsRouteWithChildren
|
|
185
209
|
'/designs': typeof DesignsRouteWithChildren
|
|
186
210
|
'/github': typeof GithubRoute
|
|
187
211
|
'/milestones': typeof MilestonesRouteWithChildren
|
|
@@ -190,11 +214,13 @@ export interface FileRoutesById {
|
|
|
190
214
|
'/search': typeof SearchRoute
|
|
191
215
|
'/tasks': typeof TasksRouteWithChildren
|
|
192
216
|
'/api/watch': typeof ApiWatchRoute
|
|
217
|
+
'/artifacts/$slug': typeof ArtifactsSlugRoute
|
|
193
218
|
'/designs/$slug': typeof DesignsSlugRoute
|
|
194
219
|
'/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
|
|
195
220
|
'/patterns/$slug': typeof PatternsSlugRoute
|
|
196
221
|
'/reports/$slug': typeof ReportsSlugRoute
|
|
197
222
|
'/tasks/$taskId': typeof TasksTaskIdRoute
|
|
223
|
+
'/artifacts/': typeof ArtifactsIndexRoute
|
|
198
224
|
'/designs/': typeof DesignsIndexRoute
|
|
199
225
|
'/milestones/': typeof MilestonesIndexRoute
|
|
200
226
|
'/patterns/': typeof PatternsIndexRoute
|
|
@@ -207,6 +233,7 @@ export interface FileRouteTypes {
|
|
|
207
233
|
fullPaths:
|
|
208
234
|
| '/'
|
|
209
235
|
| '/activity'
|
|
236
|
+
| '/artifacts'
|
|
210
237
|
| '/designs'
|
|
211
238
|
| '/github'
|
|
212
239
|
| '/milestones'
|
|
@@ -215,11 +242,13 @@ export interface FileRouteTypes {
|
|
|
215
242
|
| '/search'
|
|
216
243
|
| '/tasks'
|
|
217
244
|
| '/api/watch'
|
|
245
|
+
| '/artifacts/$slug'
|
|
218
246
|
| '/designs/$slug'
|
|
219
247
|
| '/milestones/$milestoneId'
|
|
220
248
|
| '/patterns/$slug'
|
|
221
249
|
| '/reports/$slug'
|
|
222
250
|
| '/tasks/$taskId'
|
|
251
|
+
| '/artifacts/'
|
|
223
252
|
| '/designs/'
|
|
224
253
|
| '/milestones/'
|
|
225
254
|
| '/patterns/'
|
|
@@ -233,11 +262,13 @@ export interface FileRouteTypes {
|
|
|
233
262
|
| '/github'
|
|
234
263
|
| '/search'
|
|
235
264
|
| '/api/watch'
|
|
265
|
+
| '/artifacts/$slug'
|
|
236
266
|
| '/designs/$slug'
|
|
237
267
|
| '/milestones/$milestoneId'
|
|
238
268
|
| '/patterns/$slug'
|
|
239
269
|
| '/reports/$slug'
|
|
240
270
|
| '/tasks/$taskId'
|
|
271
|
+
| '/artifacts'
|
|
241
272
|
| '/designs'
|
|
242
273
|
| '/milestones'
|
|
243
274
|
| '/patterns'
|
|
@@ -248,6 +279,7 @@ export interface FileRouteTypes {
|
|
|
248
279
|
| '__root__'
|
|
249
280
|
| '/'
|
|
250
281
|
| '/activity'
|
|
282
|
+
| '/artifacts'
|
|
251
283
|
| '/designs'
|
|
252
284
|
| '/github'
|
|
253
285
|
| '/milestones'
|
|
@@ -256,11 +288,13 @@ export interface FileRouteTypes {
|
|
|
256
288
|
| '/search'
|
|
257
289
|
| '/tasks'
|
|
258
290
|
| '/api/watch'
|
|
291
|
+
| '/artifacts/$slug'
|
|
259
292
|
| '/designs/$slug'
|
|
260
293
|
| '/milestones/$milestoneId'
|
|
261
294
|
| '/patterns/$slug'
|
|
262
295
|
| '/reports/$slug'
|
|
263
296
|
| '/tasks/$taskId'
|
|
297
|
+
| '/artifacts/'
|
|
264
298
|
| '/designs/'
|
|
265
299
|
| '/milestones/'
|
|
266
300
|
| '/patterns/'
|
|
@@ -272,6 +306,7 @@ export interface FileRouteTypes {
|
|
|
272
306
|
export interface RootRouteChildren {
|
|
273
307
|
IndexRoute: typeof IndexRoute
|
|
274
308
|
ActivityRoute: typeof ActivityRoute
|
|
309
|
+
ArtifactsRoute: typeof ArtifactsRouteWithChildren
|
|
275
310
|
DesignsRoute: typeof DesignsRouteWithChildren
|
|
276
311
|
GithubRoute: typeof GithubRoute
|
|
277
312
|
MilestonesRoute: typeof MilestonesRouteWithChildren
|
|
@@ -334,6 +369,13 @@ declare module '@tanstack/react-router' {
|
|
|
334
369
|
preLoaderRoute: typeof DesignsRouteImport
|
|
335
370
|
parentRoute: typeof rootRouteImport
|
|
336
371
|
}
|
|
372
|
+
'/artifacts': {
|
|
373
|
+
id: '/artifacts'
|
|
374
|
+
path: '/artifacts'
|
|
375
|
+
fullPath: '/artifacts'
|
|
376
|
+
preLoaderRoute: typeof ArtifactsRouteImport
|
|
377
|
+
parentRoute: typeof rootRouteImport
|
|
378
|
+
}
|
|
337
379
|
'/activity': {
|
|
338
380
|
id: '/activity'
|
|
339
381
|
path: '/activity'
|
|
@@ -383,6 +425,13 @@ declare module '@tanstack/react-router' {
|
|
|
383
425
|
preLoaderRoute: typeof DesignsIndexRouteImport
|
|
384
426
|
parentRoute: typeof DesignsRoute
|
|
385
427
|
}
|
|
428
|
+
'/artifacts/': {
|
|
429
|
+
id: '/artifacts/'
|
|
430
|
+
path: '/'
|
|
431
|
+
fullPath: '/artifacts/'
|
|
432
|
+
preLoaderRoute: typeof ArtifactsIndexRouteImport
|
|
433
|
+
parentRoute: typeof ArtifactsRoute
|
|
434
|
+
}
|
|
386
435
|
'/tasks/$taskId': {
|
|
387
436
|
id: '/tasks/$taskId'
|
|
388
437
|
path: '/$taskId'
|
|
@@ -418,6 +467,13 @@ declare module '@tanstack/react-router' {
|
|
|
418
467
|
preLoaderRoute: typeof DesignsSlugRouteImport
|
|
419
468
|
parentRoute: typeof DesignsRoute
|
|
420
469
|
}
|
|
470
|
+
'/artifacts/$slug': {
|
|
471
|
+
id: '/artifacts/$slug'
|
|
472
|
+
path: '/$slug'
|
|
473
|
+
fullPath: '/artifacts/$slug'
|
|
474
|
+
preLoaderRoute: typeof ArtifactsSlugRouteImport
|
|
475
|
+
parentRoute: typeof ArtifactsRoute
|
|
476
|
+
}
|
|
421
477
|
'/api/watch': {
|
|
422
478
|
id: '/api/watch'
|
|
423
479
|
path: '/api/watch'
|
|
@@ -435,6 +491,20 @@ declare module '@tanstack/react-router' {
|
|
|
435
491
|
}
|
|
436
492
|
}
|
|
437
493
|
|
|
494
|
+
interface ArtifactsRouteChildren {
|
|
495
|
+
ArtifactsSlugRoute: typeof ArtifactsSlugRoute
|
|
496
|
+
ArtifactsIndexRoute: typeof ArtifactsIndexRoute
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const ArtifactsRouteChildren: ArtifactsRouteChildren = {
|
|
500
|
+
ArtifactsSlugRoute: ArtifactsSlugRoute,
|
|
501
|
+
ArtifactsIndexRoute: ArtifactsIndexRoute,
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const ArtifactsRouteWithChildren = ArtifactsRoute._addFileChildren(
|
|
505
|
+
ArtifactsRouteChildren,
|
|
506
|
+
)
|
|
507
|
+
|
|
438
508
|
interface DesignsRouteChildren {
|
|
439
509
|
DesignsSlugRoute: typeof DesignsSlugRoute
|
|
440
510
|
DesignsIndexRoute: typeof DesignsIndexRoute
|
|
@@ -504,6 +574,7 @@ const TasksRouteWithChildren = TasksRoute._addFileChildren(TasksRouteChildren)
|
|
|
504
574
|
const rootRouteChildren: RootRouteChildren = {
|
|
505
575
|
IndexRoute: IndexRoute,
|
|
506
576
|
ActivityRoute: ActivityRoute,
|
|
577
|
+
ArtifactsRoute: ArtifactsRouteWithChildren,
|
|
507
578
|
DesignsRoute: DesignsRouteWithChildren,
|
|
508
579
|
GithubRoute: GithubRoute,
|
|
509
580
|
MilestonesRoute: MilestonesRouteWithChildren,
|
package/src/routes/__root.tsx
CHANGED
|
@@ -121,12 +121,19 @@ function RootLayout() {
|
|
|
121
121
|
|
|
122
122
|
const repoParam = getRepoFromUrl()
|
|
123
123
|
// Repo param takes precedence over default local data
|
|
124
|
-
|
|
124
|
+
// In hosted mode with no repo param, default to prmichaelsen/agent-context-protocol
|
|
125
|
+
const target = repoParam
|
|
126
|
+
|| (import.meta.env.VITE_HOSTED && !context.progressData
|
|
127
|
+
? { owner: 'prmichaelsen', repo: 'agent-context-protocol' }
|
|
128
|
+
: null)
|
|
129
|
+
|
|
130
|
+
if (target) {
|
|
125
131
|
const token = getStoredToken()
|
|
126
|
-
void (fetchGitHubProgress({ data: { ...
|
|
132
|
+
void (fetchGitHubProgress({ data: { ...target, token: token || undefined } }) as Promise<GitHubResult>).then((result) => {
|
|
127
133
|
if (result.ok) {
|
|
128
134
|
setProgressData(result.data)
|
|
129
|
-
setCurrentProject(`${
|
|
135
|
+
setCurrentProject(`${target.owner}/${target.repo}`)
|
|
136
|
+
setRepoInUrl(`${target.owner}/${target.repo}`)
|
|
130
137
|
}
|
|
131
138
|
})
|
|
132
139
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentDetail } from '../components/DocumentDetail'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/artifacts/$slug')({
|
|
5
|
+
component: ArtifactDetailPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function ArtifactDetailPage() {
|
|
9
|
+
const { slug } = Route.useParams()
|
|
10
|
+
return (
|
|
11
|
+
<DocumentDetail
|
|
12
|
+
slug={slug}
|
|
13
|
+
dirPath="agent/artifacts"
|
|
14
|
+
sectionLabel="Artifacts"
|
|
15
|
+
sectionHref="/artifacts"
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentList } from '../components/DocumentList'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/artifacts/')({
|
|
5
|
+
component: ArtifactsPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function ArtifactsPage() {
|
|
9
|
+
return <DocumentList title="Artifacts" dirPath="agent/artifacts" baseTo="/artifacts" />
|
|
10
|
+
}
|
|
@@ -49,39 +49,41 @@ function MilestonesPage() {
|
|
|
49
49
|
|
|
50
50
|
if (!filtered) {
|
|
51
51
|
return (
|
|
52
|
-
<div className="p-6">
|
|
52
|
+
<div className="py-4 px-4 lg:p-6">
|
|
53
53
|
<p className="text-gray-600 text-sm">No data loaded</p>
|
|
54
54
|
</div>
|
|
55
55
|
)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
|
-
<div className="p-6">
|
|
60
|
-
<div className="flex items-center justify-between mb-4">
|
|
59
|
+
<div className="py-4 lg:p-6">
|
|
60
|
+
<div className="flex items-center justify-between mb-4 px-4 lg:px-0">
|
|
61
61
|
<h2 className="text-lg font-semibold">Milestones</h2>
|
|
62
62
|
<ViewToggle value={view} onChange={setView} />
|
|
63
63
|
</div>
|
|
64
64
|
{view !== 'kanban' && (
|
|
65
|
-
<div className="flex items-center gap-3 mb-4">
|
|
65
|
+
<div className="flex flex-col sm:flex-row sm:items-center gap-3 mb-4 px-4 lg:px-0">
|
|
66
66
|
<FilterBar status={status} onStatusChange={setStatus} />
|
|
67
|
-
<div className="w-64">
|
|
67
|
+
<div className="w-full sm:w-64">
|
|
68
68
|
<SearchInput value={search} onChange={setSearch} placeholder="Filter milestones..." />
|
|
69
69
|
</div>
|
|
70
70
|
</div>
|
|
71
71
|
)}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
<div className="overflow-x-auto">
|
|
73
|
+
{view === 'table' ? (
|
|
74
|
+
<MilestoneTable milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
75
|
+
) : view === 'tree' ? (
|
|
76
|
+
<MilestoneTree milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
77
|
+
) : view === 'kanban' ? (
|
|
78
|
+
<MilestoneKanban milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
79
|
+
) : view === 'gantt' ? (
|
|
80
|
+
<MilestoneGantt milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
81
|
+
) : (
|
|
82
|
+
<Suspense fallback={<p className="text-gray-500 text-sm">Loading graph...</p>}>
|
|
83
|
+
<DependencyGraph data={filtered} />
|
|
84
|
+
</Suspense>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
85
87
|
</div>
|
|
86
88
|
)
|
|
87
89
|
}
|