claude-plan-viewer 1.3.0 → 1.4.1
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/README.md +101 -4
- package/index.ts +238 -65
- package/openapi.json +237 -0
- package/package.json +26 -15
- package/src/api-docs.html +17 -0
- package/src/client/App.tsx +54 -9
- package/src/client/components/DetailOverlay.tsx +66 -9
- package/src/client/components/DetailPanel.tsx +63 -11
- package/src/client/components/Header.tsx +24 -5
- package/src/client/components/HelpModal.tsx +30 -7
- package/src/client/components/Markdown.tsx +37 -7
- package/src/client/components/PlanRow.tsx +15 -4
- package/src/client/components/ProjectFilter.tsx +6 -11
- package/src/client/components/SearchInput.tsx +7 -1
- package/src/client/components/index.ts +0 -1
- package/src/client/hooks/useFilters.ts +7 -7
- package/src/client/hooks/useFocusTrap.ts +70 -0
- package/src/client/hooks/useKeyboard.ts +2 -2
- package/src/client/hooks/usePlans.ts +64 -73
- package/src/client/hooks/useProjects.ts +24 -11
- package/src/client/index.tsx +1 -1
- package/src/client/types.ts +7 -1
- package/src/client/utils/api.ts +13 -4
- package/src/client/utils/formatters.ts +38 -9
- package/src/client/utils/index.ts +0 -12
- package/src/index.html +13 -0
- package/{styles.css → src/styles/styles.css} +154 -72
- package/index.html +0 -14
- package/prism.bundle.js +0 -35
- package/src/client/utils/markdown.ts +0 -123
package/openapi.json
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
{
|
|
2
|
+
"openapi": "3.0.3",
|
|
3
|
+
"info": {
|
|
4
|
+
"title": "Claude Plan Viewer API",
|
|
5
|
+
"description": "API for browsing and searching Claude Code plans",
|
|
6
|
+
"version": "1.4.0",
|
|
7
|
+
"license": {
|
|
8
|
+
"name": "MIT",
|
|
9
|
+
"url": "https://opensource.org/licenses/MIT"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"servers": [
|
|
13
|
+
{
|
|
14
|
+
"url": "/",
|
|
15
|
+
"description": "Current server"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
"paths": {
|
|
19
|
+
"/api/plans": {
|
|
20
|
+
"get": {
|
|
21
|
+
"summary": "List all plans",
|
|
22
|
+
"description": "Returns metadata for all plans without content. Use /api/plans/{filename}/content to get content.",
|
|
23
|
+
"operationId": "listPlans",
|
|
24
|
+
"responses": {
|
|
25
|
+
"200": {
|
|
26
|
+
"description": "List of plans",
|
|
27
|
+
"content": {
|
|
28
|
+
"application/json": {
|
|
29
|
+
"schema": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"plans": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": {
|
|
35
|
+
"$ref": "#/components/schemas/PlanMetadata"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"/api/plans/{filename}/content": {
|
|
47
|
+
"get": {
|
|
48
|
+
"summary": "Get plan content",
|
|
49
|
+
"description": "Returns the markdown content for a specific plan",
|
|
50
|
+
"operationId": "getPlanContent",
|
|
51
|
+
"parameters": [
|
|
52
|
+
{
|
|
53
|
+
"name": "filename",
|
|
54
|
+
"in": "path",
|
|
55
|
+
"required": true,
|
|
56
|
+
"description": "The plan filename (e.g., my-plan.md)",
|
|
57
|
+
"schema": {
|
|
58
|
+
"type": "string"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"responses": {
|
|
63
|
+
"200": {
|
|
64
|
+
"description": "Plan content",
|
|
65
|
+
"content": {
|
|
66
|
+
"application/json": {
|
|
67
|
+
"schema": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"content": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"description": "Markdown content of the plan"
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
"404": {
|
|
80
|
+
"description": "Plan not found"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"/api/projects": {
|
|
86
|
+
"get": {
|
|
87
|
+
"summary": "List all projects",
|
|
88
|
+
"description": "Returns a list of unique project names associated with plans",
|
|
89
|
+
"operationId": "listProjects",
|
|
90
|
+
"responses": {
|
|
91
|
+
"200": {
|
|
92
|
+
"description": "List of project names",
|
|
93
|
+
"content": {
|
|
94
|
+
"application/json": {
|
|
95
|
+
"schema": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {
|
|
98
|
+
"projects": {
|
|
99
|
+
"type": "array",
|
|
100
|
+
"items": {
|
|
101
|
+
"type": "string"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"/api/refresh": {
|
|
113
|
+
"post": {
|
|
114
|
+
"summary": "Refresh cache",
|
|
115
|
+
"description": "Invalidates the plan cache and reloads all plans from disk",
|
|
116
|
+
"operationId": "refreshCache",
|
|
117
|
+
"responses": {
|
|
118
|
+
"200": {
|
|
119
|
+
"description": "Cache refreshed successfully",
|
|
120
|
+
"content": {
|
|
121
|
+
"application/json": {
|
|
122
|
+
"schema": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"success": {
|
|
126
|
+
"type": "boolean"
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"/api/open": {
|
|
137
|
+
"post": {
|
|
138
|
+
"summary": "Open plan in editor",
|
|
139
|
+
"description": "Opens the specified plan file in the system's default editor",
|
|
140
|
+
"operationId": "openInEditor",
|
|
141
|
+
"requestBody": {
|
|
142
|
+
"required": true,
|
|
143
|
+
"content": {
|
|
144
|
+
"application/json": {
|
|
145
|
+
"schema": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"required": ["filepath"],
|
|
148
|
+
"properties": {
|
|
149
|
+
"filepath": {
|
|
150
|
+
"type": "string",
|
|
151
|
+
"description": "Absolute path to the plan file"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
"responses": {
|
|
159
|
+
"200": {
|
|
160
|
+
"description": "File opened successfully",
|
|
161
|
+
"content": {
|
|
162
|
+
"application/json": {
|
|
163
|
+
"schema": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"success": {
|
|
167
|
+
"type": "boolean"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
"400": {
|
|
175
|
+
"description": "Invalid path"
|
|
176
|
+
},
|
|
177
|
+
"500": {
|
|
178
|
+
"description": "Failed to open file"
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"components": {
|
|
185
|
+
"schemas": {
|
|
186
|
+
"PlanMetadata": {
|
|
187
|
+
"type": "object",
|
|
188
|
+
"properties": {
|
|
189
|
+
"filename": {
|
|
190
|
+
"type": "string",
|
|
191
|
+
"description": "Plan filename (e.g., my-plan.md)"
|
|
192
|
+
},
|
|
193
|
+
"filepath": {
|
|
194
|
+
"type": "string",
|
|
195
|
+
"description": "Absolute path to the plan file"
|
|
196
|
+
},
|
|
197
|
+
"title": {
|
|
198
|
+
"type": "string",
|
|
199
|
+
"description": "Plan title extracted from markdown heading"
|
|
200
|
+
},
|
|
201
|
+
"size": {
|
|
202
|
+
"type": "integer",
|
|
203
|
+
"description": "File size in bytes"
|
|
204
|
+
},
|
|
205
|
+
"modified": {
|
|
206
|
+
"type": "string",
|
|
207
|
+
"format": "date-time",
|
|
208
|
+
"description": "Last modification timestamp"
|
|
209
|
+
},
|
|
210
|
+
"created": {
|
|
211
|
+
"type": "string",
|
|
212
|
+
"format": "date-time",
|
|
213
|
+
"description": "Creation timestamp"
|
|
214
|
+
},
|
|
215
|
+
"lineCount": {
|
|
216
|
+
"type": "integer",
|
|
217
|
+
"description": "Number of lines in the plan"
|
|
218
|
+
},
|
|
219
|
+
"wordCount": {
|
|
220
|
+
"type": "integer",
|
|
221
|
+
"description": "Number of words in the plan"
|
|
222
|
+
},
|
|
223
|
+
"project": {
|
|
224
|
+
"type": "string",
|
|
225
|
+
"nullable": true,
|
|
226
|
+
"description": "Associated Claude Code project name"
|
|
227
|
+
},
|
|
228
|
+
"sessionId": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"nullable": true,
|
|
231
|
+
"description": "Associated Claude Code session ID"
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-plan-viewer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "A web-based viewer for Claude Code plan files",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,23 +8,28 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"index.ts",
|
|
11
|
-
"
|
|
12
|
-
"src"
|
|
13
|
-
"styles.css",
|
|
14
|
-
"prism.bundle.js"
|
|
11
|
+
"openapi.json",
|
|
12
|
+
"src"
|
|
15
13
|
],
|
|
16
14
|
"scripts": {
|
|
17
15
|
"start": "bun index.ts",
|
|
18
16
|
"dev": "bun --hot index.ts",
|
|
19
|
-
"test": "bun test
|
|
17
|
+
"test": "bun test test/",
|
|
18
|
+
"test:api": "bun test test/api.test.ts",
|
|
20
19
|
"test:e2e": "bunx playwright test",
|
|
21
|
-
"build": "bun build --compile --minify --bytecode ./index.ts --outfile ./dist/
|
|
22
|
-
"build:macos-arm64": "bun build --compile --target=bun-darwin-arm64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
23
|
-
"build:macos-x64": "bun build --compile --target=bun-darwin-x64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
24
|
-
"build:linux-x64": "bun build --compile --target=bun-linux-x64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
25
|
-
"build:linux-arm64": "bun build --compile --target=bun-linux-arm64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
26
|
-
"build:windows": "bun build --compile --target=bun-windows-x64 --minify --bytecode ./index.ts --outfile ./dist/
|
|
27
|
-
"build:all": "bun run build:macos-arm64 && bun run build:macos-x64 && bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:windows"
|
|
20
|
+
"build": "bun build --compile --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer",
|
|
21
|
+
"build:macos-arm64": "bun build --compile --target=bun-darwin-arm64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-macos-arm64",
|
|
22
|
+
"build:macos-x64": "bun build --compile --target=bun-darwin-x64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-macos-x64",
|
|
23
|
+
"build:linux-x64": "bun build --compile --target=bun-linux-x64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-linux-x64",
|
|
24
|
+
"build:linux-arm64": "bun build --compile --target=bun-linux-arm64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-linux-arm64",
|
|
25
|
+
"build:windows": "bun build --compile --target=bun-windows-x64 --minify --bytecode ./index.ts --outfile ./dist/claude-plan-viewer-windows.exe",
|
|
26
|
+
"build:all": "bun run build:macos-arm64 && bun run build:macos-x64 && bun run build:linux-x64 && bun run build:linux-arm64 && bun run build:windows",
|
|
27
|
+
"clean": "rm -rf ./dist",
|
|
28
|
+
"install:link": "bun link",
|
|
29
|
+
"install:local": "bun scripts/install-local.ts",
|
|
30
|
+
"uninstall:link": "bun unlink",
|
|
31
|
+
"uninstall:local": "bun scripts/uninstall-local.ts",
|
|
32
|
+
"format": "bunx prettier --write src"
|
|
28
33
|
},
|
|
29
34
|
"keywords": [
|
|
30
35
|
"claude",
|
|
@@ -49,8 +54,10 @@
|
|
|
49
54
|
"devDependencies": {
|
|
50
55
|
"@playwright/test": "^1.57.0",
|
|
51
56
|
"@types/bun": "latest",
|
|
57
|
+
"@types/prismjs": "^1.26.5",
|
|
52
58
|
"@types/react": "^19.2.7",
|
|
53
|
-
"@types/react-dom": "^19.2.3"
|
|
59
|
+
"@types/react-dom": "^19.2.3",
|
|
60
|
+
"@types/react-syntax-highlighter": "^15.5.13"
|
|
54
61
|
},
|
|
55
62
|
"peerDependencies": {
|
|
56
63
|
"typescript": "^5.9.3"
|
|
@@ -58,6 +65,10 @@
|
|
|
58
65
|
"dependencies": {
|
|
59
66
|
"react": "^19.2.3",
|
|
60
67
|
"react-dom": "^19.2.3",
|
|
61
|
-
"react-
|
|
68
|
+
"react-markdown": "^10.1.0",
|
|
69
|
+
"react-select": "^5.10.2",
|
|
70
|
+
"react-syntax-highlighter": "^16.1.0",
|
|
71
|
+
"remark-gfm": "^4.0.1",
|
|
72
|
+
"swr": "^2.3.8"
|
|
62
73
|
}
|
|
63
74
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Plans Viewer API</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="app"></div>
|
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
|
|
11
|
+
<script>
|
|
12
|
+
Scalar.createApiReference("#app", {
|
|
13
|
+
url: "/api/openapi.json",
|
|
14
|
+
});
|
|
15
|
+
</script>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
package/src/client/App.tsx
CHANGED
|
@@ -16,6 +16,7 @@ export function App() {
|
|
|
16
16
|
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
|
17
17
|
const [showOverlay, setShowOverlay] = useState(false);
|
|
18
18
|
const [showHelp, setShowHelp] = useState(false);
|
|
19
|
+
const [copied, setCopied] = useState(false);
|
|
19
20
|
|
|
20
21
|
// Filter state
|
|
21
22
|
const {
|
|
@@ -35,11 +36,11 @@ export function App() {
|
|
|
35
36
|
// Convert Set to array for API
|
|
36
37
|
const projectsArray = useMemo(
|
|
37
38
|
() => Array.from(selectedProjects),
|
|
38
|
-
[selectedProjects]
|
|
39
|
+
[selectedProjects],
|
|
39
40
|
);
|
|
40
41
|
|
|
41
42
|
// Fetch plans with server-side filtering
|
|
42
|
-
const { plans, loading, refresh, ensureContent } = usePlans({
|
|
43
|
+
const { plans, loading, refreshing, refresh, ensureContent } = usePlans({
|
|
43
44
|
q: debouncedSearch,
|
|
44
45
|
sort: sortKey,
|
|
45
46
|
dir: sortDir,
|
|
@@ -52,6 +53,15 @@ export function App() {
|
|
|
52
53
|
// Load content when plan is selected
|
|
53
54
|
const handleSelectPlan = useCallback(
|
|
54
55
|
async (plan: Plan | null) => {
|
|
56
|
+
// Update URL query parameter
|
|
57
|
+
const url = new URL(window.location.href);
|
|
58
|
+
if (plan) {
|
|
59
|
+
url.searchParams.set("plan", plan.filename);
|
|
60
|
+
} else {
|
|
61
|
+
url.searchParams.delete("plan");
|
|
62
|
+
}
|
|
63
|
+
window.history.replaceState({}, "", url.toString());
|
|
64
|
+
|
|
55
65
|
if (plan) {
|
|
56
66
|
const withContent = await ensureContent(plan);
|
|
57
67
|
setSelectedPlan(withContent);
|
|
@@ -59,7 +69,7 @@ export function App() {
|
|
|
59
69
|
setSelectedPlan(null);
|
|
60
70
|
}
|
|
61
71
|
},
|
|
62
|
-
[ensureContent]
|
|
72
|
+
[ensureContent],
|
|
63
73
|
);
|
|
64
74
|
|
|
65
75
|
// Open in editor
|
|
@@ -74,6 +84,20 @@ export function App() {
|
|
|
74
84
|
navigator.clipboard.writeText(`claude --resume ${sessionId}`);
|
|
75
85
|
}, []);
|
|
76
86
|
|
|
87
|
+
// Copy filepath
|
|
88
|
+
const handleCopyFilepath = useCallback((filepath: string) => {
|
|
89
|
+
navigator.clipboard.writeText(filepath);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// Copy plan content
|
|
93
|
+
const handleCopyPlan = useCallback(() => {
|
|
94
|
+
if (selectedPlan?.content) {
|
|
95
|
+
navigator.clipboard.writeText(selectedPlan.content);
|
|
96
|
+
setCopied(true);
|
|
97
|
+
setTimeout(() => setCopied(false), 2000);
|
|
98
|
+
}
|
|
99
|
+
}, [selectedPlan]);
|
|
100
|
+
|
|
77
101
|
// Clear search
|
|
78
102
|
const handleClearSearch = useCallback(() => {
|
|
79
103
|
setSearchQuery("");
|
|
@@ -92,19 +116,34 @@ export function App() {
|
|
|
92
116
|
onClearSearch: handleClearSearch,
|
|
93
117
|
});
|
|
94
118
|
|
|
95
|
-
//
|
|
119
|
+
// Select plan from URL or auto-select first plan
|
|
96
120
|
useEffect(() => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
121
|
+
if (plans.length === 0) return;
|
|
122
|
+
|
|
123
|
+
// Check URL for plan parameter
|
|
124
|
+
const url = new URL(window.location.href);
|
|
125
|
+
const planFromUrl = url.searchParams.get("plan");
|
|
126
|
+
|
|
127
|
+
if (planFromUrl) {
|
|
128
|
+
const matchingPlan = plans.find((p) => p.filename === planFromUrl);
|
|
129
|
+
if (matchingPlan && selectedPlan?.filename !== planFromUrl) {
|
|
130
|
+
handleSelectPlan(matchingPlan);
|
|
131
|
+
return;
|
|
101
132
|
}
|
|
102
133
|
}
|
|
134
|
+
|
|
135
|
+
// Fall back to selecting first plan if nothing selected
|
|
136
|
+
if (!selectedPlan) {
|
|
137
|
+
handleSelectPlan(plans[0]);
|
|
138
|
+
}
|
|
103
139
|
}, [plans, selectedPlan, handleSelectPlan]);
|
|
104
140
|
|
|
105
141
|
// Clear selection if selected plan is no longer in results
|
|
106
142
|
useEffect(() => {
|
|
107
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
selectedPlan &&
|
|
145
|
+
!plans.find((p) => p.filename === selectedPlan.filename)
|
|
146
|
+
) {
|
|
108
147
|
setSelectedPlan(null);
|
|
109
148
|
}
|
|
110
149
|
}, [plans, selectedPlan]);
|
|
@@ -130,6 +169,7 @@ export function App() {
|
|
|
130
169
|
onToggleProject={toggleProject}
|
|
131
170
|
onClearProjects={clearProjects}
|
|
132
171
|
onRefresh={refresh}
|
|
172
|
+
refreshing={refreshing}
|
|
133
173
|
/>
|
|
134
174
|
|
|
135
175
|
{plans.length === 0 ? (
|
|
@@ -156,6 +196,9 @@ export function App() {
|
|
|
156
196
|
onOpenEditor={handleOpenEditor}
|
|
157
197
|
onToggleOverlay={() => setShowOverlay(true)}
|
|
158
198
|
onCopySession={handleCopySession}
|
|
199
|
+
onCopyFilepath={handleCopyFilepath}
|
|
200
|
+
onCopyPlan={handleCopyPlan}
|
|
201
|
+
copied={copied}
|
|
159
202
|
/>
|
|
160
203
|
|
|
161
204
|
{showOverlay && selectedPlan && (
|
|
@@ -164,6 +207,8 @@ export function App() {
|
|
|
164
207
|
onClose={() => setShowOverlay(false)}
|
|
165
208
|
onOpenEditor={handleOpenEditor}
|
|
166
209
|
onCopySession={handleCopySession}
|
|
210
|
+
onCopyPlan={handleCopyPlan}
|
|
211
|
+
copied={copied}
|
|
167
212
|
/>
|
|
168
213
|
)}
|
|
169
214
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useCallback } from "react";
|
|
1
|
+
import { useEffect, useCallback, useRef } from "react";
|
|
2
|
+
import { useFocusTrap } from "../hooks/useFocusTrap";
|
|
2
3
|
import type { Plan } from "../types.ts";
|
|
3
4
|
import { formatFullDate, formatSize } from "../utils/formatters.ts";
|
|
4
5
|
import { Markdown } from "./Markdown.tsx";
|
|
@@ -8,6 +9,8 @@ interface DetailOverlayProps {
|
|
|
8
9
|
onClose: () => void;
|
|
9
10
|
onOpenEditor: () => void;
|
|
10
11
|
onCopySession: (sessionId: string) => void;
|
|
12
|
+
onCopyPlan: () => void;
|
|
13
|
+
copied: boolean;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export function DetailOverlay({
|
|
@@ -15,7 +18,12 @@ export function DetailOverlay({
|
|
|
15
18
|
onClose,
|
|
16
19
|
onOpenEditor,
|
|
17
20
|
onCopySession,
|
|
21
|
+
onCopyPlan,
|
|
22
|
+
copied,
|
|
18
23
|
}: DetailOverlayProps) {
|
|
24
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
useFocusTrap(panelRef, true);
|
|
26
|
+
|
|
19
27
|
const handleKeyDown = useCallback(
|
|
20
28
|
(e: KeyboardEvent) => {
|
|
21
29
|
if (e.key === "Escape" || e.key === "f") {
|
|
@@ -23,7 +31,7 @@ export function DetailOverlay({
|
|
|
23
31
|
onClose();
|
|
24
32
|
}
|
|
25
33
|
},
|
|
26
|
-
[onClose]
|
|
34
|
+
[onClose],
|
|
27
35
|
);
|
|
28
36
|
|
|
29
37
|
useEffect(() => {
|
|
@@ -43,11 +51,20 @@ export function DetailOverlay({
|
|
|
43
51
|
aria-hidden="false"
|
|
44
52
|
onClick={onClose}
|
|
45
53
|
>
|
|
46
|
-
<div
|
|
54
|
+
<div
|
|
55
|
+
ref={panelRef}
|
|
56
|
+
className="detail-overlay-panel"
|
|
57
|
+
role="dialog"
|
|
58
|
+
aria-modal="true"
|
|
59
|
+
aria-labelledby="detail-overlay-title"
|
|
60
|
+
onClick={(e) => e.stopPropagation()}
|
|
61
|
+
>
|
|
47
62
|
<div className="detail-overlay-bar">
|
|
48
63
|
<div className="detail-meta detail-overlay-meta">
|
|
49
|
-
{plan.project &&
|
|
50
|
-
|
|
64
|
+
{plan.project && (
|
|
65
|
+
<span className="project-tag">{plan.project}</span>
|
|
66
|
+
)}
|
|
67
|
+
<span id="detail-overlay-title">{plan.filename}</span>
|
|
51
68
|
<span>{formatFullDate(plan.modified)}</span>
|
|
52
69
|
<span>{formatSize(plan.size)}</span>
|
|
53
70
|
<span>{plan.lineCount} lines</span>
|
|
@@ -61,14 +78,42 @@ export function DetailOverlay({
|
|
|
61
78
|
</button>
|
|
62
79
|
)}
|
|
63
80
|
</div>
|
|
81
|
+
<button
|
|
82
|
+
className={copied ? "action-btn copied" : "action-btn"}
|
|
83
|
+
onClick={onCopyPlan}
|
|
84
|
+
title="Copy plan to clipboard"
|
|
85
|
+
>
|
|
86
|
+
<svg
|
|
87
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
88
|
+
viewBox="0 0 24 24"
|
|
89
|
+
fill="none"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
strokeWidth="2"
|
|
92
|
+
strokeLinecap="round"
|
|
93
|
+
strokeLinejoin="round"
|
|
94
|
+
className="icon"
|
|
95
|
+
>
|
|
96
|
+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
|
|
97
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
98
|
+
</svg>
|
|
99
|
+
</button>
|
|
64
100
|
<button
|
|
65
101
|
className="action-btn"
|
|
66
102
|
onClick={onOpenEditor}
|
|
67
103
|
title="Open in editor (Enter)"
|
|
68
104
|
>
|
|
69
|
-
<svg
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
<svg
|
|
106
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
107
|
+
viewBox="0 0 24 24"
|
|
108
|
+
fill="none"
|
|
109
|
+
stroke="currentColor"
|
|
110
|
+
strokeWidth="2"
|
|
111
|
+
strokeLinecap="round"
|
|
112
|
+
strokeLinejoin="round"
|
|
113
|
+
className="icon"
|
|
114
|
+
>
|
|
115
|
+
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
|
|
116
|
+
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
|
|
72
117
|
</svg>
|
|
73
118
|
</button>
|
|
74
119
|
<button
|
|
@@ -76,7 +121,19 @@ export function DetailOverlay({
|
|
|
76
121
|
onClick={onClose}
|
|
77
122
|
title="Close fullscreen (Esc or F)"
|
|
78
123
|
>
|
|
79
|
-
|
|
124
|
+
<svg
|
|
125
|
+
width="14"
|
|
126
|
+
height="14"
|
|
127
|
+
viewBox="0 0 24 24"
|
|
128
|
+
fill="none"
|
|
129
|
+
stroke="currentColor"
|
|
130
|
+
strokeWidth="2.5"
|
|
131
|
+
strokeLinecap="round"
|
|
132
|
+
strokeLinejoin="round"
|
|
133
|
+
>
|
|
134
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
135
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
136
|
+
</svg>
|
|
80
137
|
</button>
|
|
81
138
|
</div>
|
|
82
139
|
<div className="detail-overlay-content">
|