@siteboon/claude-code-ui 1.8.2 → 1.8.4
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/dist/assets/index-CeR_JfKq.js +895 -0
- package/dist/assets/index-Co7ALK3i.css +32 -0
- package/{index.html → dist/index.html} +2 -1
- package/package.json +6 -1
- package/server/database/auth.db +0 -0
- package/.env.example +0 -12
- package/.nvmrc +0 -1
- package/postcss.config.js +0 -6
- package/src/App.jsx +0 -751
- package/src/components/ChatInterface.jsx +0 -3485
- package/src/components/ClaudeLogo.jsx +0 -11
- package/src/components/ClaudeStatus.jsx +0 -107
- package/src/components/CodeEditor.jsx +0 -422
- package/src/components/CreateTaskModal.jsx +0 -88
- package/src/components/CursorLogo.jsx +0 -9
- package/src/components/DarkModeToggle.jsx +0 -35
- package/src/components/DiffViewer.jsx +0 -41
- package/src/components/ErrorBoundary.jsx +0 -73
- package/src/components/FileTree.jsx +0 -480
- package/src/components/GitPanel.jsx +0 -1283
- package/src/components/ImageViewer.jsx +0 -54
- package/src/components/LoginForm.jsx +0 -110
- package/src/components/MainContent.jsx +0 -577
- package/src/components/MicButton.jsx +0 -272
- package/src/components/MobileNav.jsx +0 -88
- package/src/components/NextTaskBanner.jsx +0 -695
- package/src/components/PRDEditor.jsx +0 -871
- package/src/components/ProtectedRoute.jsx +0 -44
- package/src/components/QuickSettingsPanel.jsx +0 -262
- package/src/components/Settings.jsx +0 -2023
- package/src/components/SetupForm.jsx +0 -135
- package/src/components/Shell.jsx +0 -663
- package/src/components/Sidebar.jsx +0 -1665
- package/src/components/StandaloneShell.jsx +0 -106
- package/src/components/TaskCard.jsx +0 -210
- package/src/components/TaskDetail.jsx +0 -406
- package/src/components/TaskIndicator.jsx +0 -108
- package/src/components/TaskList.jsx +0 -1054
- package/src/components/TaskMasterSetupWizard.jsx +0 -603
- package/src/components/TaskMasterStatus.jsx +0 -86
- package/src/components/TodoList.jsx +0 -91
- package/src/components/Tooltip.jsx +0 -91
- package/src/components/ui/badge.jsx +0 -31
- package/src/components/ui/button.jsx +0 -46
- package/src/components/ui/input.jsx +0 -19
- package/src/components/ui/scroll-area.jsx +0 -23
- package/src/contexts/AuthContext.jsx +0 -158
- package/src/contexts/TaskMasterContext.jsx +0 -324
- package/src/contexts/TasksSettingsContext.jsx +0 -95
- package/src/contexts/ThemeContext.jsx +0 -94
- package/src/contexts/WebSocketContext.jsx +0 -29
- package/src/hooks/useAudioRecorder.js +0 -109
- package/src/hooks/useVersionCheck.js +0 -39
- package/src/index.css +0 -822
- package/src/lib/utils.js +0 -6
- package/src/main.jsx +0 -10
- package/src/utils/api.js +0 -141
- package/src/utils/websocket.js +0 -109
- package/src/utils/whisper.js +0 -37
- package/tailwind.config.js +0 -63
- package/vite.config.js +0 -29
- /package/{public → dist}/convert-icons.md +0 -0
- /package/{public → dist}/favicon.png +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/generate-icons.js +0 -0
- /package/{public → dist}/icons/claude-ai-icon.svg +0 -0
- /package/{public → dist}/icons/cursor.svg +0 -0
- /package/{public → dist}/icons/generate-icons.md +0 -0
- /package/{public → dist}/icons/icon-128x128.png +0 -0
- /package/{public → dist}/icons/icon-128x128.svg +0 -0
- /package/{public → dist}/icons/icon-144x144.png +0 -0
- /package/{public → dist}/icons/icon-144x144.svg +0 -0
- /package/{public → dist}/icons/icon-152x152.png +0 -0
- /package/{public → dist}/icons/icon-152x152.svg +0 -0
- /package/{public → dist}/icons/icon-192x192.png +0 -0
- /package/{public → dist}/icons/icon-192x192.svg +0 -0
- /package/{public → dist}/icons/icon-384x384.png +0 -0
- /package/{public → dist}/icons/icon-384x384.svg +0 -0
- /package/{public → dist}/icons/icon-512x512.png +0 -0
- /package/{public → dist}/icons/icon-512x512.svg +0 -0
- /package/{public → dist}/icons/icon-72x72.png +0 -0
- /package/{public → dist}/icons/icon-72x72.svg +0 -0
- /package/{public → dist}/icons/icon-96x96.png +0 -0
- /package/{public → dist}/icons/icon-96x96.svg +0 -0
- /package/{public → dist}/icons/icon-template.svg +0 -0
- /package/{public → dist}/logo.svg +0 -0
- /package/{public → dist}/manifest.json +0 -0
- /package/{public → dist}/screenshots/cli-selection.png +0 -0
- /package/{public → dist}/screenshots/desktop-main.png +0 -0
- /package/{public → dist}/screenshots/mobile-chat.png +0 -0
- /package/{public → dist}/screenshots/tools-modal.png +0 -0
- /package/{public → dist}/sw.js +0 -0
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
function DiffViewer({ diff, fileName, isMobile, wrapText }) {
|
|
4
|
-
if (!diff) {
|
|
5
|
-
return (
|
|
6
|
-
<div className="p-4 text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
7
|
-
No diff available
|
|
8
|
-
</div>
|
|
9
|
-
);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const renderDiffLine = (line, index) => {
|
|
13
|
-
const isAddition = line.startsWith('+') && !line.startsWith('+++');
|
|
14
|
-
const isDeletion = line.startsWith('-') && !line.startsWith('---');
|
|
15
|
-
const isHeader = line.startsWith('@@');
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<div
|
|
19
|
-
key={index}
|
|
20
|
-
className={`font-mono text-xs p-2 ${
|
|
21
|
-
isMobile && wrapText ? 'whitespace-pre-wrap break-all' : 'whitespace-pre overflow-x-auto'
|
|
22
|
-
} ${
|
|
23
|
-
isAddition ? 'bg-green-50 dark:bg-green-950 text-green-700 dark:text-green-300' :
|
|
24
|
-
isDeletion ? 'bg-red-50 dark:bg-red-950 text-red-700 dark:text-red-300' :
|
|
25
|
-
isHeader ? 'bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300' :
|
|
26
|
-
'text-gray-600 dark:text-gray-400'
|
|
27
|
-
}`}
|
|
28
|
-
>
|
|
29
|
-
{line}
|
|
30
|
-
</div>
|
|
31
|
-
);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<div className="diff-viewer">
|
|
36
|
-
{diff.split('\n').map((line, index) => renderDiffLine(line, index))}
|
|
37
|
-
</div>
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export default DiffViewer;
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
class ErrorBoundary extends React.Component {
|
|
4
|
-
constructor(props) {
|
|
5
|
-
super(props);
|
|
6
|
-
this.state = { hasError: false, error: null, errorInfo: null };
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
static getDerivedStateFromError(error) {
|
|
10
|
-
// Update state so the next render will show the fallback UI
|
|
11
|
-
return { hasError: true };
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
componentDidCatch(error, errorInfo) {
|
|
15
|
-
// Log the error details
|
|
16
|
-
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
17
|
-
|
|
18
|
-
// You can also log the error to an error reporting service here
|
|
19
|
-
this.setState({
|
|
20
|
-
error: error,
|
|
21
|
-
errorInfo: errorInfo
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
render() {
|
|
26
|
-
if (this.state.hasError) {
|
|
27
|
-
// Fallback UI
|
|
28
|
-
return (
|
|
29
|
-
<div className="flex flex-col items-center justify-center p-8 text-center">
|
|
30
|
-
<div className="bg-red-50 border border-red-200 rounded-lg p-6 max-w-md">
|
|
31
|
-
<div className="flex items-center mb-4">
|
|
32
|
-
<div className="flex-shrink-0">
|
|
33
|
-
<svg className="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
34
|
-
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
|
35
|
-
</svg>
|
|
36
|
-
</div>
|
|
37
|
-
<h3 className="ml-3 text-sm font-medium text-red-800">
|
|
38
|
-
Something went wrong
|
|
39
|
-
</h3>
|
|
40
|
-
</div>
|
|
41
|
-
<div className="text-sm text-red-700">
|
|
42
|
-
<p className="mb-2">An error occurred while loading the chat interface.</p>
|
|
43
|
-
{this.props.showDetails && this.state.error && (
|
|
44
|
-
<details className="mt-4">
|
|
45
|
-
<summary className="cursor-pointer text-xs font-mono">Error Details</summary>
|
|
46
|
-
<pre className="mt-2 text-xs bg-red-100 p-2 rounded overflow-auto max-h-40">
|
|
47
|
-
{this.state.error.toString()}
|
|
48
|
-
{this.state.errorInfo && this.state.errorInfo.componentStack}
|
|
49
|
-
</pre>
|
|
50
|
-
</details>
|
|
51
|
-
)}
|
|
52
|
-
</div>
|
|
53
|
-
<div className="mt-4">
|
|
54
|
-
<button
|
|
55
|
-
onClick={() => {
|
|
56
|
-
this.setState({ hasError: false, error: null, errorInfo: null });
|
|
57
|
-
if (this.props.onRetry) this.props.onRetry();
|
|
58
|
-
}}
|
|
59
|
-
className="bg-red-600 text-white px-4 py-2 rounded text-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500"
|
|
60
|
-
>
|
|
61
|
-
Try Again
|
|
62
|
-
</button>
|
|
63
|
-
</div>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return this.props.children;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export default ErrorBoundary;
|
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { ScrollArea } from './ui/scroll-area';
|
|
3
|
-
import { Button } from './ui/button';
|
|
4
|
-
import { Input } from './ui/input';
|
|
5
|
-
import { Folder, FolderOpen, File, FileText, FileCode, List, TableProperties, Eye, Search, X } from 'lucide-react';
|
|
6
|
-
import { cn } from '../lib/utils';
|
|
7
|
-
import CodeEditor from './CodeEditor';
|
|
8
|
-
import ImageViewer from './ImageViewer';
|
|
9
|
-
import { api } from '../utils/api';
|
|
10
|
-
|
|
11
|
-
function FileTree({ selectedProject }) {
|
|
12
|
-
const [files, setFiles] = useState([]);
|
|
13
|
-
const [loading, setLoading] = useState(false);
|
|
14
|
-
const [expandedDirs, setExpandedDirs] = useState(new Set());
|
|
15
|
-
const [selectedFile, setSelectedFile] = useState(null);
|
|
16
|
-
const [selectedImage, setSelectedImage] = useState(null);
|
|
17
|
-
const [viewMode, setViewMode] = useState('detailed'); // 'simple', 'detailed', 'compact'
|
|
18
|
-
const [searchQuery, setSearchQuery] = useState('');
|
|
19
|
-
const [filteredFiles, setFilteredFiles] = useState([]);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (selectedProject) {
|
|
23
|
-
fetchFiles();
|
|
24
|
-
}
|
|
25
|
-
}, [selectedProject]);
|
|
26
|
-
|
|
27
|
-
// Load view mode preference from localStorage
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
const savedViewMode = localStorage.getItem('file-tree-view-mode');
|
|
30
|
-
if (savedViewMode && ['simple', 'detailed', 'compact'].includes(savedViewMode)) {
|
|
31
|
-
setViewMode(savedViewMode);
|
|
32
|
-
}
|
|
33
|
-
}, []);
|
|
34
|
-
|
|
35
|
-
// Filter files based on search query
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (!searchQuery.trim()) {
|
|
38
|
-
setFilteredFiles(files);
|
|
39
|
-
} else {
|
|
40
|
-
const filtered = filterFiles(files, searchQuery.toLowerCase());
|
|
41
|
-
setFilteredFiles(filtered);
|
|
42
|
-
|
|
43
|
-
// Auto-expand directories that contain matches
|
|
44
|
-
const expandMatches = (items) => {
|
|
45
|
-
items.forEach(item => {
|
|
46
|
-
if (item.type === 'directory' && item.children && item.children.length > 0) {
|
|
47
|
-
setExpandedDirs(prev => new Set(prev.add(item.path)));
|
|
48
|
-
expandMatches(item.children);
|
|
49
|
-
}
|
|
50
|
-
});
|
|
51
|
-
};
|
|
52
|
-
expandMatches(filtered);
|
|
53
|
-
}
|
|
54
|
-
}, [files, searchQuery]);
|
|
55
|
-
|
|
56
|
-
// Recursively filter files and directories based on search query
|
|
57
|
-
const filterFiles = (items, query) => {
|
|
58
|
-
return items.reduce((filtered, item) => {
|
|
59
|
-
const matchesName = item.name.toLowerCase().includes(query);
|
|
60
|
-
let filteredChildren = [];
|
|
61
|
-
|
|
62
|
-
if (item.type === 'directory' && item.children) {
|
|
63
|
-
filteredChildren = filterFiles(item.children, query);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Include item if:
|
|
67
|
-
// 1. It matches the search query, or
|
|
68
|
-
// 2. It's a directory with matching children
|
|
69
|
-
if (matchesName || filteredChildren.length > 0) {
|
|
70
|
-
filtered.push({
|
|
71
|
-
...item,
|
|
72
|
-
children: filteredChildren
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return filtered;
|
|
77
|
-
}, []);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const fetchFiles = async () => {
|
|
81
|
-
setLoading(true);
|
|
82
|
-
try {
|
|
83
|
-
const response = await api.getFiles(selectedProject.name);
|
|
84
|
-
|
|
85
|
-
if (!response.ok) {
|
|
86
|
-
const errorText = await response.text();
|
|
87
|
-
console.error('❌ File fetch failed:', response.status, errorText);
|
|
88
|
-
setFiles([]);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const data = await response.json();
|
|
93
|
-
setFiles(data);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.error('❌ Error fetching files:', error);
|
|
96
|
-
setFiles([]);
|
|
97
|
-
} finally {
|
|
98
|
-
setLoading(false);
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const toggleDirectory = (path) => {
|
|
103
|
-
const newExpanded = new Set(expandedDirs);
|
|
104
|
-
if (newExpanded.has(path)) {
|
|
105
|
-
newExpanded.delete(path);
|
|
106
|
-
} else {
|
|
107
|
-
newExpanded.add(path);
|
|
108
|
-
}
|
|
109
|
-
setExpandedDirs(newExpanded);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// Change view mode and save preference
|
|
113
|
-
const changeViewMode = (mode) => {
|
|
114
|
-
setViewMode(mode);
|
|
115
|
-
localStorage.setItem('file-tree-view-mode', mode);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// Format file size
|
|
119
|
-
const formatFileSize = (bytes) => {
|
|
120
|
-
if (!bytes || bytes === 0) return '0 B';
|
|
121
|
-
const k = 1024;
|
|
122
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
123
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
124
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// Format date as relative time
|
|
128
|
-
const formatRelativeTime = (date) => {
|
|
129
|
-
if (!date) return '-';
|
|
130
|
-
const now = new Date();
|
|
131
|
-
const past = new Date(date);
|
|
132
|
-
const diffInSeconds = Math.floor((now - past) / 1000);
|
|
133
|
-
|
|
134
|
-
if (diffInSeconds < 60) return 'just now';
|
|
135
|
-
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} min ago`;
|
|
136
|
-
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
|
|
137
|
-
if (diffInSeconds < 2592000) return `${Math.floor(diffInSeconds / 86400)} days ago`;
|
|
138
|
-
return past.toLocaleDateString();
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const renderFileTree = (items, level = 0) => {
|
|
142
|
-
return items.map((item) => (
|
|
143
|
-
<div key={item.path} className="select-none">
|
|
144
|
-
<Button
|
|
145
|
-
variant="ghost"
|
|
146
|
-
className={cn(
|
|
147
|
-
"w-full justify-start p-2 h-auto font-normal text-left hover:bg-accent",
|
|
148
|
-
)}
|
|
149
|
-
style={{ paddingLeft: `${level * 16 + 12}px` }}
|
|
150
|
-
onClick={() => {
|
|
151
|
-
if (item.type === 'directory') {
|
|
152
|
-
toggleDirectory(item.path);
|
|
153
|
-
} else if (isImageFile(item.name)) {
|
|
154
|
-
// Open image in viewer
|
|
155
|
-
setSelectedImage({
|
|
156
|
-
name: item.name,
|
|
157
|
-
path: item.path,
|
|
158
|
-
projectPath: selectedProject.path,
|
|
159
|
-
projectName: selectedProject.name
|
|
160
|
-
});
|
|
161
|
-
} else {
|
|
162
|
-
// Open file in editor
|
|
163
|
-
setSelectedFile({
|
|
164
|
-
name: item.name,
|
|
165
|
-
path: item.path,
|
|
166
|
-
projectPath: selectedProject.path,
|
|
167
|
-
projectName: selectedProject.name
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}}
|
|
171
|
-
>
|
|
172
|
-
<div className="flex items-center gap-2 min-w-0 w-full">
|
|
173
|
-
{item.type === 'directory' ? (
|
|
174
|
-
expandedDirs.has(item.path) ? (
|
|
175
|
-
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
176
|
-
) : (
|
|
177
|
-
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
178
|
-
)
|
|
179
|
-
) : (
|
|
180
|
-
getFileIcon(item.name)
|
|
181
|
-
)}
|
|
182
|
-
<span className="text-sm truncate text-foreground">
|
|
183
|
-
{item.name}
|
|
184
|
-
</span>
|
|
185
|
-
</div>
|
|
186
|
-
</Button>
|
|
187
|
-
|
|
188
|
-
{item.type === 'directory' &&
|
|
189
|
-
expandedDirs.has(item.path) &&
|
|
190
|
-
item.children &&
|
|
191
|
-
item.children.length > 0 && (
|
|
192
|
-
<div>
|
|
193
|
-
{renderFileTree(item.children, level + 1)}
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
196
|
-
</div>
|
|
197
|
-
));
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const isImageFile = (filename) => {
|
|
201
|
-
const ext = filename.split('.').pop()?.toLowerCase();
|
|
202
|
-
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'];
|
|
203
|
-
return imageExtensions.includes(ext);
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
const getFileIcon = (filename) => {
|
|
207
|
-
const ext = filename.split('.').pop()?.toLowerCase();
|
|
208
|
-
|
|
209
|
-
const codeExtensions = ['js', 'jsx', 'ts', 'tsx', 'py', 'java', 'cpp', 'c', 'php', 'rb', 'go', 'rs'];
|
|
210
|
-
const docExtensions = ['md', 'txt', 'doc', 'pdf'];
|
|
211
|
-
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp'];
|
|
212
|
-
|
|
213
|
-
if (codeExtensions.includes(ext)) {
|
|
214
|
-
return <FileCode className="w-4 h-4 text-green-500 flex-shrink-0" />;
|
|
215
|
-
} else if (docExtensions.includes(ext)) {
|
|
216
|
-
return <FileText className="w-4 h-4 text-blue-500 flex-shrink-0" />;
|
|
217
|
-
} else if (imageExtensions.includes(ext)) {
|
|
218
|
-
return <File className="w-4 h-4 text-purple-500 flex-shrink-0" />;
|
|
219
|
-
} else {
|
|
220
|
-
return <File className="w-4 h-4 text-muted-foreground flex-shrink-0" />;
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
// Render detailed view with table-like layout
|
|
225
|
-
const renderDetailedView = (items, level = 0) => {
|
|
226
|
-
return items.map((item) => (
|
|
227
|
-
<div key={item.path} className="select-none">
|
|
228
|
-
<div
|
|
229
|
-
className={cn(
|
|
230
|
-
"grid grid-cols-12 gap-2 p-2 hover:bg-accent cursor-pointer items-center",
|
|
231
|
-
)}
|
|
232
|
-
style={{ paddingLeft: `${level * 16 + 12}px` }}
|
|
233
|
-
onClick={() => {
|
|
234
|
-
if (item.type === 'directory') {
|
|
235
|
-
toggleDirectory(item.path);
|
|
236
|
-
} else if (isImageFile(item.name)) {
|
|
237
|
-
setSelectedImage({
|
|
238
|
-
name: item.name,
|
|
239
|
-
path: item.path,
|
|
240
|
-
projectPath: selectedProject.path,
|
|
241
|
-
projectName: selectedProject.name
|
|
242
|
-
});
|
|
243
|
-
} else {
|
|
244
|
-
setSelectedFile({
|
|
245
|
-
name: item.name,
|
|
246
|
-
path: item.path,
|
|
247
|
-
projectPath: selectedProject.path,
|
|
248
|
-
projectName: selectedProject.name
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}}
|
|
252
|
-
>
|
|
253
|
-
<div className="col-span-5 flex items-center gap-2 min-w-0">
|
|
254
|
-
{item.type === 'directory' ? (
|
|
255
|
-
expandedDirs.has(item.path) ? (
|
|
256
|
-
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
257
|
-
) : (
|
|
258
|
-
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
259
|
-
)
|
|
260
|
-
) : (
|
|
261
|
-
getFileIcon(item.name)
|
|
262
|
-
)}
|
|
263
|
-
<span className="text-sm truncate text-foreground">
|
|
264
|
-
{item.name}
|
|
265
|
-
</span>
|
|
266
|
-
</div>
|
|
267
|
-
<div className="col-span-2 text-sm text-muted-foreground">
|
|
268
|
-
{item.type === 'file' ? formatFileSize(item.size) : '-'}
|
|
269
|
-
</div>
|
|
270
|
-
<div className="col-span-3 text-sm text-muted-foreground">
|
|
271
|
-
{formatRelativeTime(item.modified)}
|
|
272
|
-
</div>
|
|
273
|
-
<div className="col-span-2 text-sm text-muted-foreground font-mono">
|
|
274
|
-
{item.permissionsRwx || '-'}
|
|
275
|
-
</div>
|
|
276
|
-
</div>
|
|
277
|
-
|
|
278
|
-
{item.type === 'directory' &&
|
|
279
|
-
expandedDirs.has(item.path) &&
|
|
280
|
-
item.children &&
|
|
281
|
-
renderDetailedView(item.children, level + 1)}
|
|
282
|
-
</div>
|
|
283
|
-
));
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
// Render compact view with inline details
|
|
287
|
-
const renderCompactView = (items, level = 0) => {
|
|
288
|
-
return items.map((item) => (
|
|
289
|
-
<div key={item.path} className="select-none">
|
|
290
|
-
<div
|
|
291
|
-
className={cn(
|
|
292
|
-
"flex items-center justify-between p-2 hover:bg-accent cursor-pointer",
|
|
293
|
-
)}
|
|
294
|
-
style={{ paddingLeft: `${level * 16 + 12}px` }}
|
|
295
|
-
onClick={() => {
|
|
296
|
-
if (item.type === 'directory') {
|
|
297
|
-
toggleDirectory(item.path);
|
|
298
|
-
} else if (isImageFile(item.name)) {
|
|
299
|
-
setSelectedImage({
|
|
300
|
-
name: item.name,
|
|
301
|
-
path: item.path,
|
|
302
|
-
projectPath: selectedProject.path,
|
|
303
|
-
projectName: selectedProject.name
|
|
304
|
-
});
|
|
305
|
-
} else {
|
|
306
|
-
setSelectedFile({
|
|
307
|
-
name: item.name,
|
|
308
|
-
path: item.path,
|
|
309
|
-
projectPath: selectedProject.path,
|
|
310
|
-
projectName: selectedProject.name
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
}}
|
|
314
|
-
>
|
|
315
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
316
|
-
{item.type === 'directory' ? (
|
|
317
|
-
expandedDirs.has(item.path) ? (
|
|
318
|
-
<FolderOpen className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
|
319
|
-
) : (
|
|
320
|
-
<Folder className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
321
|
-
)
|
|
322
|
-
) : (
|
|
323
|
-
getFileIcon(item.name)
|
|
324
|
-
)}
|
|
325
|
-
<span className="text-sm truncate text-foreground">
|
|
326
|
-
{item.name}
|
|
327
|
-
</span>
|
|
328
|
-
</div>
|
|
329
|
-
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
330
|
-
{item.type === 'file' && (
|
|
331
|
-
<>
|
|
332
|
-
<span>{formatFileSize(item.size)}</span>
|
|
333
|
-
<span className="font-mono">{item.permissionsRwx}</span>
|
|
334
|
-
</>
|
|
335
|
-
)}
|
|
336
|
-
</div>
|
|
337
|
-
</div>
|
|
338
|
-
|
|
339
|
-
{item.type === 'directory' &&
|
|
340
|
-
expandedDirs.has(item.path) &&
|
|
341
|
-
item.children &&
|
|
342
|
-
renderCompactView(item.children, level + 1)}
|
|
343
|
-
</div>
|
|
344
|
-
));
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
if (loading) {
|
|
348
|
-
return (
|
|
349
|
-
<div className="h-full flex items-center justify-center">
|
|
350
|
-
<div className="text-gray-500 dark:text-gray-400">
|
|
351
|
-
Loading files...
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return (
|
|
358
|
-
<div className="h-full flex flex-col bg-card">
|
|
359
|
-
{/* Header with Search and View Mode Toggle */}
|
|
360
|
-
<div className="p-4 border-b border-border space-y-3">
|
|
361
|
-
<div className="flex items-center justify-between">
|
|
362
|
-
<h3 className="text-sm font-medium text-foreground">Files</h3>
|
|
363
|
-
<div className="flex gap-1">
|
|
364
|
-
<Button
|
|
365
|
-
variant={viewMode === 'simple' ? 'default' : 'ghost'}
|
|
366
|
-
size="sm"
|
|
367
|
-
className="h-8 w-8 p-0"
|
|
368
|
-
onClick={() => changeViewMode('simple')}
|
|
369
|
-
title="Simple view"
|
|
370
|
-
>
|
|
371
|
-
<List className="w-4 h-4" />
|
|
372
|
-
</Button>
|
|
373
|
-
<Button
|
|
374
|
-
variant={viewMode === 'compact' ? 'default' : 'ghost'}
|
|
375
|
-
size="sm"
|
|
376
|
-
className="h-8 w-8 p-0"
|
|
377
|
-
onClick={() => changeViewMode('compact')}
|
|
378
|
-
title="Compact view"
|
|
379
|
-
>
|
|
380
|
-
<Eye className="w-4 h-4" />
|
|
381
|
-
</Button>
|
|
382
|
-
<Button
|
|
383
|
-
variant={viewMode === 'detailed' ? 'default' : 'ghost'}
|
|
384
|
-
size="sm"
|
|
385
|
-
className="h-8 w-8 p-0"
|
|
386
|
-
onClick={() => changeViewMode('detailed')}
|
|
387
|
-
title="Detailed view"
|
|
388
|
-
>
|
|
389
|
-
<TableProperties className="w-4 h-4" />
|
|
390
|
-
</Button>
|
|
391
|
-
</div>
|
|
392
|
-
</div>
|
|
393
|
-
|
|
394
|
-
{/* Search Bar */}
|
|
395
|
-
<div className="relative">
|
|
396
|
-
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
397
|
-
<Input
|
|
398
|
-
type="text"
|
|
399
|
-
placeholder="Search files and folders..."
|
|
400
|
-
value={searchQuery}
|
|
401
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
402
|
-
className="pl-8 pr-8 h-8 text-sm"
|
|
403
|
-
/>
|
|
404
|
-
{searchQuery && (
|
|
405
|
-
<Button
|
|
406
|
-
variant="ghost"
|
|
407
|
-
size="sm"
|
|
408
|
-
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0 hover:bg-accent"
|
|
409
|
-
onClick={() => setSearchQuery('')}
|
|
410
|
-
title="Clear search"
|
|
411
|
-
>
|
|
412
|
-
<X className="w-3 h-3" />
|
|
413
|
-
</Button>
|
|
414
|
-
)}
|
|
415
|
-
</div>
|
|
416
|
-
</div>
|
|
417
|
-
|
|
418
|
-
{/* Column Headers for Detailed View */}
|
|
419
|
-
{viewMode === 'detailed' && filteredFiles.length > 0 && (
|
|
420
|
-
<div className="px-4 pt-2 pb-1 border-b border-border">
|
|
421
|
-
<div className="grid grid-cols-12 gap-2 px-2 text-xs font-medium text-muted-foreground">
|
|
422
|
-
<div className="col-span-5">Name</div>
|
|
423
|
-
<div className="col-span-2">Size</div>
|
|
424
|
-
<div className="col-span-3">Modified</div>
|
|
425
|
-
<div className="col-span-2">Permissions</div>
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
)}
|
|
429
|
-
|
|
430
|
-
<ScrollArea className="flex-1 p-4">
|
|
431
|
-
{files.length === 0 ? (
|
|
432
|
-
<div className="text-center py-8">
|
|
433
|
-
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
|
|
434
|
-
<Folder className="w-6 h-6 text-muted-foreground" />
|
|
435
|
-
</div>
|
|
436
|
-
<h4 className="font-medium text-foreground mb-1">No files found</h4>
|
|
437
|
-
<p className="text-sm text-muted-foreground">
|
|
438
|
-
Check if the project path is accessible
|
|
439
|
-
</p>
|
|
440
|
-
</div>
|
|
441
|
-
) : filteredFiles.length === 0 && searchQuery ? (
|
|
442
|
-
<div className="text-center py-8">
|
|
443
|
-
<div className="w-12 h-12 bg-muted rounded-lg flex items-center justify-center mx-auto mb-3">
|
|
444
|
-
<Search className="w-6 h-6 text-muted-foreground" />
|
|
445
|
-
</div>
|
|
446
|
-
<h4 className="font-medium text-foreground mb-1">No matches found</h4>
|
|
447
|
-
<p className="text-sm text-muted-foreground">
|
|
448
|
-
Try a different search term or clear the search
|
|
449
|
-
</p>
|
|
450
|
-
</div>
|
|
451
|
-
) : (
|
|
452
|
-
<div className={viewMode === 'detailed' ? '' : 'space-y-1'}>
|
|
453
|
-
{viewMode === 'simple' && renderFileTree(filteredFiles)}
|
|
454
|
-
{viewMode === 'compact' && renderCompactView(filteredFiles)}
|
|
455
|
-
{viewMode === 'detailed' && renderDetailedView(filteredFiles)}
|
|
456
|
-
</div>
|
|
457
|
-
)}
|
|
458
|
-
</ScrollArea>
|
|
459
|
-
|
|
460
|
-
{/* Code Editor Modal */}
|
|
461
|
-
{selectedFile && (
|
|
462
|
-
<CodeEditor
|
|
463
|
-
file={selectedFile}
|
|
464
|
-
onClose={() => setSelectedFile(null)}
|
|
465
|
-
projectPath={selectedFile.projectPath}
|
|
466
|
-
/>
|
|
467
|
-
)}
|
|
468
|
-
|
|
469
|
-
{/* Image Viewer Modal */}
|
|
470
|
-
{selectedImage && (
|
|
471
|
-
<ImageViewer
|
|
472
|
-
file={selectedImage}
|
|
473
|
-
onClose={() => setSelectedImage(null)}
|
|
474
|
-
/>
|
|
475
|
-
)}
|
|
476
|
-
</div>
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
export default FileTree;
|