@hienlh/ppm 0.13.15 → 0.13.16
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/CHANGELOG.md +22 -0
- package/CLAUDE.md +5 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/bun.lock +2135 -0
- package/bunfig.toml +2 -0
- package/dist/web/assets/{audio-preview-YOG6Biao.js → audio-preview-bQ4k3Rdv.js} +1 -1
- package/dist/web/assets/{chat-tab-DbdDJuLu.js → chat-tab-DISlwA7-.js} +3 -3
- package/dist/web/assets/code-editor-Cni2pSOw.js +8 -0
- package/dist/web/assets/{conflict-editor-DnGfriL5.js → conflict-editor-82mk659D.js} +1 -1
- package/dist/web/assets/{database-viewer-AodppoTs.js → database-viewer-eCnvGdDi.js} +1 -1
- package/dist/web/assets/{diff-viewer-DykLUwna.js → diff-viewer-cezBVQp6.js} +1 -1
- package/dist/web/assets/{extension-webview-Bck7QuaB.js → extension-webview-B5dN_Qrm.js} +1 -1
- package/dist/web/assets/file-store-DOxcU_7s.js +1 -0
- package/dist/web/assets/{glide-data-grid-BVt0mwcA.js → glide-data-grid-yscGXxJe.js} +1 -1
- package/dist/web/assets/{image-preview-DaSmrIvY.js → image-preview-CGdBnOP0.js} +1 -1
- package/dist/web/assets/index-C_pdjLi6.js +27 -0
- package/dist/web/assets/index-nC9UURj4.css +2 -0
- package/dist/web/assets/keybindings-store-LHrHsvXn.js +1 -0
- package/dist/web/assets/{markdown-renderer-B1me_hz2.js → markdown-renderer-DF-Ga1mN.js} +1 -1
- package/dist/web/assets/{pdf-preview-Dci7TIL1.js → pdf-preview-C15gYiMf.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-BeM40G-J.js → port-forwarding-tab-BcpVh4oH.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CGVBOwA9.js → postgres-viewer-DqcY70o6.js} +1 -1
- package/dist/web/assets/{settings-tab-CYS8VfNl.js → settings-tab-CMso6o_A.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DstPySPF.js → sql-query-editor-C0Lq3NYC.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-SUGEk_G1.js → sqlite-viewer-CFHqKvjt.js} +1 -1
- package/dist/web/assets/{terminal-tab-CJvjF79J.js → terminal-tab-ej7HGI3k.js} +1 -1
- package/dist/web/assets/{video-preview-gJSKmPQr.js → video-preview-BZLGMaKk.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -0
- package/src/server/routes/files.ts +15 -0
- package/src/services/file.service.ts +15 -0
- package/src/web/components/editor/editor-breadcrumb.tsx +88 -36
- package/src/web/components/explorer/file-actions.tsx +12 -129
- package/src/web/components/explorer/file-icon-map.ts +69 -0
- package/src/web/components/explorer/file-tree.tsx +177 -362
- package/src/web/components/explorer/inline-tree-input.tsx +120 -0
- package/src/web/components/explorer/tree-node-context-menu.tsx +97 -0
- package/src/web/components/explorer/tree-node.tsx +343 -0
- package/src/web/components/explorer/use-file-upload-drag.ts +77 -0
- package/src/web/components/explorer/use-tree-keyboard-nav.ts +126 -0
- package/src/web/components/layout/mobile-nav.tsx +73 -84
- package/src/web/components/layout/project-bottom-sheet.tsx +61 -82
- package/src/web/components/ui/adaptive-context-menu.tsx +245 -0
- package/src/web/components/ui/mobile-bottom-sheet.tsx +155 -0
- package/src/web/hooks/use-is-mobile.ts +28 -0
- package/src/web/hooks/use-swipe-to-dismiss.ts +46 -0
- package/src/web/stores/file-store.ts +74 -3
- package/src/web/stores/git-status-store.ts +87 -2
- package/dist/web/assets/code-editor-C4nuAsy6.js +0 -8
- package/dist/web/assets/file-store-4BpOJthN.js +0 -1
- package/dist/web/assets/index-CSK33ACc.css +0 -2
- package/dist/web/assets/index-gZKF1YKy.js +0 -27
- package/dist/web/assets/keybindings-store-DBKLTPrk.js +0 -1
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { useFileStore, type FileNode } from "@/stores/file-store";
|
|
13
13
|
import { useShallow } from "zustand/react/shallow";
|
|
14
14
|
import { useTabStore } from "@/stores/tab-store";
|
|
15
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
15
16
|
import { basename } from "@/lib/utils";
|
|
16
17
|
|
|
17
18
|
const ICON_MAP: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
@@ -34,11 +35,14 @@ interface BreadcrumbSegment {
|
|
|
34
35
|
fullPath: string;
|
|
35
36
|
node: FileNode | null;
|
|
36
37
|
siblings: FileNode[];
|
|
38
|
+
/** Folder path whose children are the siblings (empty string = root) */
|
|
39
|
+
parentPath: string;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
function walkTree(tree: FileNode[], segments: string[]): BreadcrumbSegment[] {
|
|
40
43
|
const result: BreadcrumbSegment[] = [];
|
|
41
44
|
let current: FileNode[] = tree;
|
|
45
|
+
let parentPath = "";
|
|
42
46
|
|
|
43
47
|
for (let i = 0; i < segments.length; i++) {
|
|
44
48
|
const seg = segments[i]!;
|
|
@@ -49,17 +53,20 @@ function walkTree(tree: FileNode[], segments: string[]): BreadcrumbSegment[] {
|
|
|
49
53
|
fullPath,
|
|
50
54
|
node: match ?? null,
|
|
51
55
|
siblings: current,
|
|
56
|
+
parentPath,
|
|
52
57
|
});
|
|
53
58
|
if (match?.children) {
|
|
59
|
+
parentPath = match.path;
|
|
54
60
|
current = match.children;
|
|
55
61
|
} else {
|
|
56
|
-
// Remaining segments
|
|
62
|
+
// Remaining segments — parent children not loaded yet
|
|
57
63
|
for (let j = i + 1; j < segments.length; j++) {
|
|
58
64
|
result.push({
|
|
59
65
|
name: segments[j]!,
|
|
60
66
|
fullPath: segments.slice(0, j + 1).join("/"),
|
|
61
67
|
node: null,
|
|
62
68
|
siblings: [],
|
|
69
|
+
parentPath: segments.slice(0, j).join("/"),
|
|
63
70
|
});
|
|
64
71
|
}
|
|
65
72
|
break;
|
|
@@ -85,11 +92,23 @@ interface EditorBreadcrumbProps {
|
|
|
85
92
|
export function EditorBreadcrumb({ filePath, projectName, tabId, className }: EditorBreadcrumbProps) {
|
|
86
93
|
const tree = useFileStore((s) => s.tree);
|
|
87
94
|
const { updateTab, openTab } = useTabStore(useShallow((s) => ({ updateTab: s.updateTab, openTab: s.openTab })));
|
|
95
|
+
const projectPath = useProjectStore((s) => s.projects.find((p) => p.name === projectName)?.path ?? "");
|
|
88
96
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
89
97
|
|
|
98
|
+
// Strip project root prefix so segments align with the relative-path file tree
|
|
99
|
+
const { prefixParts, relativePath } = useMemo(() => {
|
|
100
|
+
const norm = filePath.startsWith("/") ? filePath.slice(1) : filePath;
|
|
101
|
+
const normRoot = projectPath.startsWith("/") ? projectPath.slice(1) : projectPath;
|
|
102
|
+
if (normRoot && norm.startsWith(normRoot + "/")) {
|
|
103
|
+
const rel = norm.slice(normRoot.length + 1);
|
|
104
|
+
return { prefixParts: normRoot.split("/"), relativePath: rel };
|
|
105
|
+
}
|
|
106
|
+
return { prefixParts: [] as string[], relativePath: norm };
|
|
107
|
+
}, [filePath, projectPath]);
|
|
108
|
+
|
|
90
109
|
const segments = useMemo(
|
|
91
|
-
() => walkTree(tree,
|
|
92
|
-
[tree,
|
|
110
|
+
() => walkTree(tree, relativePath.split("/").filter(Boolean)),
|
|
111
|
+
[tree, relativePath],
|
|
93
112
|
);
|
|
94
113
|
|
|
95
114
|
// Auto-scroll to rightmost segment
|
|
@@ -110,19 +129,21 @@ export function EditorBreadcrumb({ filePath, projectName, tabId, className }: Ed
|
|
|
110
129
|
|
|
111
130
|
return (
|
|
112
131
|
<div ref={scrollRef} className={className}>
|
|
132
|
+
{prefixParts.map((part, i) => (
|
|
133
|
+
<div key={`prefix-${i}`} className="flex items-center shrink-0">
|
|
134
|
+
{i > 0 && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
|
|
135
|
+
<span className="text-xs text-muted-foreground px-1 py-0.5">{part}</span>
|
|
136
|
+
</div>
|
|
137
|
+
))}
|
|
113
138
|
{segments.map((seg, i) => (
|
|
114
139
|
<div key={seg.fullPath} className="flex items-center shrink-0">
|
|
115
|
-
{i > 0 && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
/>
|
|
123
|
-
) : (
|
|
124
|
-
<span className="text-xs text-muted-foreground px-1 py-0.5">{seg.name}</span>
|
|
125
|
-
)}
|
|
140
|
+
{(i > 0 || prefixParts.length > 0) && <ChevronRight className="size-3 text-muted-foreground shrink-0 mx-0.5" />}
|
|
141
|
+
<SegmentDropdown
|
|
142
|
+
segment={seg}
|
|
143
|
+
isLast={i === segments.length - 1}
|
|
144
|
+
projectName={projectName}
|
|
145
|
+
onFileClick={handleFileClick}
|
|
146
|
+
/>
|
|
126
147
|
</div>
|
|
127
148
|
))}
|
|
128
149
|
</div>
|
|
@@ -137,10 +158,19 @@ interface SegmentDropdownProps {
|
|
|
137
158
|
}
|
|
138
159
|
|
|
139
160
|
function SegmentDropdown({ segment, isLast, projectName, onFileClick }: SegmentDropdownProps) {
|
|
161
|
+
const loadChildren = useFileStore((s) => s.loadChildren);
|
|
162
|
+
const loadedPaths = useFileStore((s) => s.loadedPaths);
|
|
140
163
|
const sorted = useMemo(() => sortNodes(segment.siblings), [segment.siblings]);
|
|
164
|
+
const isLoaded = loadedPaths.has(segment.parentPath);
|
|
165
|
+
|
|
166
|
+
function handleOpenChange(open: boolean) {
|
|
167
|
+
if (open && !isLoaded) {
|
|
168
|
+
loadChildren(projectName, segment.parentPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
141
171
|
|
|
142
172
|
return (
|
|
143
|
-
<DropdownMenu>
|
|
173
|
+
<DropdownMenu onOpenChange={handleOpenChange}>
|
|
144
174
|
<DropdownMenuTrigger asChild>
|
|
145
175
|
<button
|
|
146
176
|
type="button"
|
|
@@ -152,15 +182,21 @@ function SegmentDropdown({ segment, isLast, projectName, onFileClick }: SegmentD
|
|
|
152
182
|
</button>
|
|
153
183
|
</DropdownMenuTrigger>
|
|
154
184
|
<DropdownMenuContent align="start" className="max-h-[300px] p-1">
|
|
155
|
-
{sorted.
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
185
|
+
{sorted.length === 0 ? (
|
|
186
|
+
<DropdownMenuItem disabled className="text-xs text-muted-foreground">
|
|
187
|
+
Loading…
|
|
188
|
+
</DropdownMenuItem>
|
|
189
|
+
) : (
|
|
190
|
+
sorted.map((node) => (
|
|
191
|
+
<NodeMenuItem
|
|
192
|
+
key={node.path}
|
|
193
|
+
node={node}
|
|
194
|
+
projectName={projectName}
|
|
195
|
+
activePath={segment.fullPath}
|
|
196
|
+
onFileClick={onFileClick}
|
|
197
|
+
/>
|
|
198
|
+
))
|
|
199
|
+
)}
|
|
164
200
|
</DropdownMenuContent>
|
|
165
201
|
</DropdownMenu>
|
|
166
202
|
);
|
|
@@ -176,24 +212,41 @@ interface NodeMenuItemProps {
|
|
|
176
212
|
function NodeMenuItem({ node, projectName, activePath, onFileClick }: NodeMenuItemProps) {
|
|
177
213
|
const Icon = getIcon(node.name, node.type === "directory");
|
|
178
214
|
const isActive = node.path === activePath;
|
|
215
|
+
const loadChildren = useFileStore((s) => s.loadChildren);
|
|
216
|
+
const loadedPaths = useFileStore((s) => s.loadedPaths);
|
|
217
|
+
|
|
218
|
+
if (node.type === "directory") {
|
|
219
|
+
const children = node.children ?? [];
|
|
220
|
+
const isLoaded = loadedPaths.has(node.path);
|
|
221
|
+
|
|
222
|
+
function handleSubOpen(open: boolean) {
|
|
223
|
+
if (open && !isLoaded) {
|
|
224
|
+
loadChildren(projectName, node.path);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
179
227
|
|
|
180
|
-
if (node.type === "directory" && node.children && node.children.length > 0) {
|
|
181
228
|
return (
|
|
182
|
-
<DropdownMenuSub>
|
|
229
|
+
<DropdownMenuSub onOpenChange={handleSubOpen}>
|
|
183
230
|
<DropdownMenuSubTrigger className={`text-xs gap-1.5 ${isActive ? "bg-muted" : ""}`}>
|
|
184
231
|
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
185
232
|
<span className="truncate">{node.name}</span>
|
|
186
233
|
</DropdownMenuSubTrigger>
|
|
187
234
|
<DropdownMenuSubContent className="max-h-[300px] overflow-y-auto p-1">
|
|
188
|
-
{
|
|
189
|
-
<
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
235
|
+
{children.length === 0 ? (
|
|
236
|
+
<DropdownMenuItem disabled className="text-xs text-muted-foreground">
|
|
237
|
+
Loading…
|
|
238
|
+
</DropdownMenuItem>
|
|
239
|
+
) : (
|
|
240
|
+
sortNodes(children).map((child) => (
|
|
241
|
+
<NodeMenuItem
|
|
242
|
+
key={child.path}
|
|
243
|
+
node={child}
|
|
244
|
+
projectName={projectName}
|
|
245
|
+
activePath={activePath}
|
|
246
|
+
onFileClick={onFileClick}
|
|
247
|
+
/>
|
|
248
|
+
))
|
|
249
|
+
)}
|
|
197
250
|
</DropdownMenuSubContent>
|
|
198
251
|
</DropdownMenuSub>
|
|
199
252
|
);
|
|
@@ -206,7 +259,6 @@ function NodeMenuItem({ node, projectName, activePath, onFileClick }: NodeMenuIt
|
|
|
206
259
|
// onSelect doesn't give MouseEvent, use click handler for Ctrl detection
|
|
207
260
|
}}
|
|
208
261
|
onClick={(e) => {
|
|
209
|
-
if (node.type === "directory") return;
|
|
210
262
|
onFileClick(node.path, e);
|
|
211
263
|
}}
|
|
212
264
|
>
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* FileActions — delete confirmation dialog.
|
|
3
|
+
* Create and rename are handled inline by InlineTreeInput.
|
|
4
|
+
*/
|
|
5
|
+
import { useState } from "react";
|
|
2
6
|
import { api, projectUrl } from "@/lib/api-client";
|
|
3
7
|
import type { FileNode } from "@/stores/file-store";
|
|
4
8
|
import {
|
|
@@ -10,10 +14,9 @@ import {
|
|
|
10
14
|
DialogFooter,
|
|
11
15
|
} from "@/components/ui/dialog";
|
|
12
16
|
import { Button } from "@/components/ui/button";
|
|
13
|
-
import { Input } from "@/components/ui/input";
|
|
14
17
|
|
|
15
18
|
interface FileActionsProps {
|
|
16
|
-
action:
|
|
19
|
+
action: "delete";
|
|
17
20
|
node: FileNode;
|
|
18
21
|
projectName: string;
|
|
19
22
|
onClose: () => void;
|
|
@@ -21,73 +24,13 @@ interface FileActionsProps {
|
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export function FileActions({
|
|
24
|
-
action,
|
|
25
27
|
node,
|
|
26
28
|
projectName,
|
|
27
29
|
onClose,
|
|
28
30
|
onRefresh,
|
|
29
31
|
}: FileActionsProps) {
|
|
30
|
-
const [name, setName] = useState("");
|
|
31
32
|
const [loading, setLoading] = useState(false);
|
|
32
33
|
const [error, setError] = useState<string | null>(null);
|
|
33
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (action === "rename") {
|
|
37
|
-
setName(node.name);
|
|
38
|
-
} else {
|
|
39
|
-
setName("");
|
|
40
|
-
}
|
|
41
|
-
}, [action, node.name]);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
// Focus input after dialog mounts
|
|
45
|
-
const timer = setTimeout(() => inputRef.current?.focus(), 100);
|
|
46
|
-
return () => clearTimeout(timer);
|
|
47
|
-
}, []);
|
|
48
|
-
|
|
49
|
-
async function handleCreate(type: "file" | "directory") {
|
|
50
|
-
if (!name.trim()) return;
|
|
51
|
-
setLoading(true);
|
|
52
|
-
setError(null);
|
|
53
|
-
try {
|
|
54
|
-
const parentPath = node.type === "directory" ? node.path : node.path.split("/").slice(0, -1).join("/");
|
|
55
|
-
const fullPath = parentPath ? `${parentPath}/${name.trim()}` : name.trim();
|
|
56
|
-
await api.post(`${projectUrl(projectName)}/files/create`, {
|
|
57
|
-
path: fullPath,
|
|
58
|
-
type,
|
|
59
|
-
});
|
|
60
|
-
onRefresh();
|
|
61
|
-
onClose();
|
|
62
|
-
} catch (err) {
|
|
63
|
-
setError(err instanceof Error ? err.message : "Failed to create");
|
|
64
|
-
} finally {
|
|
65
|
-
setLoading(false);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function handleRename() {
|
|
70
|
-
if (!name.trim() || name.trim() === node.name) {
|
|
71
|
-
onClose();
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
setLoading(true);
|
|
75
|
-
setError(null);
|
|
76
|
-
try {
|
|
77
|
-
const parentPath = node.path.split("/").slice(0, -1).join("/");
|
|
78
|
-
const newPath = parentPath ? `${parentPath}/${name.trim()}` : name.trim();
|
|
79
|
-
await api.post(`${projectUrl(projectName)}/files/rename`, {
|
|
80
|
-
oldPath: node.path,
|
|
81
|
-
newPath,
|
|
82
|
-
});
|
|
83
|
-
onRefresh();
|
|
84
|
-
onClose();
|
|
85
|
-
} catch (err) {
|
|
86
|
-
setError(err instanceof Error ? err.message : "Failed to rename");
|
|
87
|
-
} finally {
|
|
88
|
-
setLoading(false);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
34
|
|
|
92
35
|
async function handleDelete() {
|
|
93
36
|
setLoading(true);
|
|
@@ -105,84 +48,24 @@ export function FileActions({
|
|
|
105
48
|
}
|
|
106
49
|
}
|
|
107
50
|
|
|
108
|
-
function handleKeyDown(e: React.KeyboardEvent) {
|
|
109
|
-
if (e.key === "Enter") {
|
|
110
|
-
if (action === "new-file") handleCreate("file");
|
|
111
|
-
else if (action === "new-folder") handleCreate("directory");
|
|
112
|
-
else if (action === "rename") handleRename();
|
|
113
|
-
}
|
|
114
|
-
if (e.key === "Escape") onClose();
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (action === "delete") {
|
|
118
|
-
return (
|
|
119
|
-
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
120
|
-
<DialogContent>
|
|
121
|
-
<DialogHeader>
|
|
122
|
-
<DialogTitle>Delete {node.type === "directory" ? "Folder" : "File"}</DialogTitle>
|
|
123
|
-
<DialogDescription>
|
|
124
|
-
Are you sure you want to delete{" "}
|
|
125
|
-
<span className="font-mono font-semibold">{node.name}</span>?
|
|
126
|
-
{node.type === "directory" && " This will delete all contents."}
|
|
127
|
-
</DialogDescription>
|
|
128
|
-
</DialogHeader>
|
|
129
|
-
{error && <p className="text-sm text-error">{error}</p>}
|
|
130
|
-
<DialogFooter>
|
|
131
|
-
<Button variant="outline" onClick={onClose} disabled={loading}>
|
|
132
|
-
Cancel
|
|
133
|
-
</Button>
|
|
134
|
-
<Button variant="destructive" onClick={handleDelete} disabled={loading}>
|
|
135
|
-
{loading ? "Deleting..." : "Delete"}
|
|
136
|
-
</Button>
|
|
137
|
-
</DialogFooter>
|
|
138
|
-
</DialogContent>
|
|
139
|
-
</Dialog>
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const title =
|
|
144
|
-
action === "new-file"
|
|
145
|
-
? "New File"
|
|
146
|
-
: action === "new-folder"
|
|
147
|
-
? "New Folder"
|
|
148
|
-
: "Rename";
|
|
149
|
-
|
|
150
|
-
const placeholder =
|
|
151
|
-
action === "rename" ? node.name : action === "new-file" ? "filename.ts" : "folder-name";
|
|
152
|
-
|
|
153
51
|
return (
|
|
154
52
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
155
53
|
<DialogContent>
|
|
156
54
|
<DialogHeader>
|
|
157
|
-
<DialogTitle>{
|
|
55
|
+
<DialogTitle>Delete {node.type === "directory" ? "Folder" : "File"}</DialogTitle>
|
|
158
56
|
<DialogDescription>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
57
|
+
Are you sure you want to delete{" "}
|
|
58
|
+
<span className="font-mono font-semibold">{node.name}</span>?
|
|
59
|
+
{node.type === "directory" && " This will delete all contents."}
|
|
162
60
|
</DialogDescription>
|
|
163
61
|
</DialogHeader>
|
|
164
|
-
<Input
|
|
165
|
-
ref={inputRef}
|
|
166
|
-
value={name}
|
|
167
|
-
onChange={(e) => setName(e.target.value)}
|
|
168
|
-
onKeyDown={handleKeyDown}
|
|
169
|
-
placeholder={placeholder}
|
|
170
|
-
disabled={loading}
|
|
171
|
-
/>
|
|
172
62
|
{error && <p className="text-sm text-error">{error}</p>}
|
|
173
63
|
<DialogFooter>
|
|
174
64
|
<Button variant="outline" onClick={onClose} disabled={loading}>
|
|
175
65
|
Cancel
|
|
176
66
|
</Button>
|
|
177
|
-
<Button
|
|
178
|
-
|
|
179
|
-
if (action === "new-file") handleCreate("file");
|
|
180
|
-
else if (action === "new-folder") handleCreate("directory");
|
|
181
|
-
else handleRename();
|
|
182
|
-
}}
|
|
183
|
-
disabled={loading || !name.trim()}
|
|
184
|
-
>
|
|
185
|
-
{loading ? "Saving..." : action === "rename" ? "Rename" : "Create"}
|
|
67
|
+
<Button variant="destructive" onClick={handleDelete} disabled={loading}>
|
|
68
|
+
{loading ? "Deleting..." : "Delete"}
|
|
186
69
|
</Button>
|
|
187
70
|
</DialogFooter>
|
|
188
71
|
</DialogContent>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File extension → icon + color mapping for the file tree explorer.
|
|
3
|
+
* Extracted from file-tree.tsx for modularity.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
File,
|
|
7
|
+
FileCode,
|
|
8
|
+
FileJson,
|
|
9
|
+
FileText,
|
|
10
|
+
FileType,
|
|
11
|
+
FileImage,
|
|
12
|
+
FileVideo,
|
|
13
|
+
FileAudio,
|
|
14
|
+
FileSpreadsheet,
|
|
15
|
+
FileArchive,
|
|
16
|
+
Database,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
|
|
19
|
+
export type FileIconInfo = { icon: React.ComponentType<{ className?: string }>; color?: string };
|
|
20
|
+
|
|
21
|
+
const FILE_ICON_MAP: Record<string, FileIconInfo> = {
|
|
22
|
+
// Code
|
|
23
|
+
ts: { icon: FileCode, color: "text-blue-400" }, tsx: { icon: FileCode, color: "text-blue-400" },
|
|
24
|
+
js: { icon: FileCode, color: "text-yellow-400" }, jsx: { icon: FileCode, color: "text-yellow-400" },
|
|
25
|
+
py: { icon: FileCode, color: "text-green-400" }, rs: { icon: FileCode, color: "text-orange-400" },
|
|
26
|
+
go: { icon: FileCode, color: "text-cyan-400" }, c: { icon: FileCode, color: "text-blue-300" },
|
|
27
|
+
cpp: { icon: FileCode, color: "text-blue-300" }, java: { icon: FileCode, color: "text-red-400" },
|
|
28
|
+
rb: { icon: FileCode, color: "text-red-400" }, php: { icon: FileCode, color: "text-purple-400" },
|
|
29
|
+
swift: { icon: FileCode, color: "text-orange-400" }, kt: { icon: FileCode, color: "text-purple-400" },
|
|
30
|
+
dart: { icon: FileCode, color: "text-cyan-400" }, sh: { icon: FileCode, color: "text-green-300" },
|
|
31
|
+
html: { icon: FileCode, color: "text-orange-400" }, css: { icon: FileCode, color: "text-blue-400" },
|
|
32
|
+
scss: { icon: FileCode, color: "text-pink-400" },
|
|
33
|
+
// Data
|
|
34
|
+
json: { icon: FileJson, color: "text-yellow-400" },
|
|
35
|
+
yaml: { icon: FileType, color: "text-orange-300" }, yml: { icon: FileType, color: "text-orange-300" },
|
|
36
|
+
toml: { icon: FileType, color: "text-orange-300" }, ini: { icon: FileType, color: "text-orange-300" },
|
|
37
|
+
env: { icon: FileType, color: "text-yellow-300" },
|
|
38
|
+
csv: { icon: FileSpreadsheet, color: "text-green-400" },
|
|
39
|
+
xls: { icon: FileSpreadsheet, color: "text-green-400" }, xlsx: { icon: FileSpreadsheet, color: "text-green-400" },
|
|
40
|
+
// Text/Docs
|
|
41
|
+
md: { icon: FileText, color: "text-text-secondary" }, txt: { icon: FileText, color: "text-text-secondary" },
|
|
42
|
+
log: { icon: FileText, color: "text-text-subtle" }, pdf: { icon: FileText, color: "text-red-400" },
|
|
43
|
+
// Images
|
|
44
|
+
png: { icon: FileImage, color: "text-green-400" }, jpg: { icon: FileImage, color: "text-green-400" },
|
|
45
|
+
jpeg: { icon: FileImage, color: "text-green-400" }, gif: { icon: FileImage, color: "text-green-400" },
|
|
46
|
+
svg: { icon: FileImage, color: "text-yellow-400" }, webp: { icon: FileImage, color: "text-green-400" },
|
|
47
|
+
ico: { icon: FileImage, color: "text-green-400" }, bmp: { icon: FileImage, color: "text-green-400" },
|
|
48
|
+
// Video
|
|
49
|
+
mp4: { icon: FileVideo, color: "text-purple-400" }, webm: { icon: FileVideo, color: "text-purple-400" },
|
|
50
|
+
mov: { icon: FileVideo, color: "text-purple-400" }, avi: { icon: FileVideo, color: "text-purple-400" },
|
|
51
|
+
mkv: { icon: FileVideo, color: "text-purple-400" },
|
|
52
|
+
// Audio
|
|
53
|
+
mp3: { icon: FileAudio, color: "text-pink-400" }, wav: { icon: FileAudio, color: "text-pink-400" },
|
|
54
|
+
ogg: { icon: FileAudio, color: "text-pink-400" }, flac: { icon: FileAudio, color: "text-pink-400" },
|
|
55
|
+
// Database
|
|
56
|
+
db: { icon: Database, color: "text-amber-400" }, sqlite: { icon: Database, color: "text-amber-400" },
|
|
57
|
+
sqlite3: { icon: Database, color: "text-amber-400" }, sql: { icon: Database, color: "text-amber-400" },
|
|
58
|
+
// Archives
|
|
59
|
+
zip: { icon: FileArchive, color: "text-amber-300" }, tar: { icon: FileArchive, color: "text-amber-300" },
|
|
60
|
+
gz: { icon: FileArchive, color: "text-amber-300" }, rar: { icon: FileArchive, color: "text-amber-300" },
|
|
61
|
+
"7z": { icon: FileArchive, color: "text-amber-300" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const DEFAULT_FILE_ICON: FileIconInfo = { icon: File };
|
|
65
|
+
|
|
66
|
+
export function getFileIcon(name: string): FileIconInfo {
|
|
67
|
+
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
68
|
+
return FILE_ICON_MAP[ext] ?? DEFAULT_FILE_ICON;
|
|
69
|
+
}
|