Skip to content

Commit 246d041

Browse files
committed
feat: project config view
1 parent 8a5cb19 commit 246d041

File tree

8 files changed

+350
-141
lines changed

8 files changed

+350
-141
lines changed

package-lock.json

Lines changed: 118 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"randomatic": "^3.1.1",
7272
"react": "^18.3.1",
7373
"react-apexcharts": "^1.7.0",
74+
"react-arborist": "^3.4.3",
7475
"react-avatar": "^5.0.3",
7576
"react-complex-tree": "^2.6.1",
7677
"react-dom": "^18.3.1",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import React, { useState } from "react";
2+
3+
import { Tree, NodeRendererProps } from "react-arborist";
4+
5+
import { IconSvg } from "@components/atoms";
6+
7+
import { ChevronDownIcon, TrashIcon } from "@assets/image/icons";
8+
import { FileIcon } from "@assets/image/icons/sidebar";
9+
10+
type FileTreeNode = {
11+
children?: FileTreeNode[];
12+
id: string;
13+
isFolder: boolean;
14+
name: string;
15+
};
16+
17+
interface FileTreeProps {
18+
data: FileTreeNode[];
19+
activeFilePath?: string;
20+
onFileClick: (path: string) => void;
21+
onFileDelete: (path: string) => void;
22+
height: number;
23+
}
24+
25+
interface NodeProps {
26+
node: NodeRendererProps<FileTreeNode>["node"];
27+
style: NodeRendererProps<FileTreeNode>["style"];
28+
activeFilePath?: string;
29+
onFileClick: (path: string) => void;
30+
onFileDelete: (path: string) => void;
31+
}
32+
33+
const FileNode = ({ node, style, activeFilePath, onFileClick, onFileDelete }: NodeProps) => {
34+
const [isHovered, setIsHovered] = useState(false);
35+
const isActive = !node.data.isFolder && activeFilePath === node.data.id;
36+
37+
const handleClick = () => {
38+
if (node.data.isFolder) {
39+
node.toggle();
40+
} else {
41+
onFileClick(node.data.id);
42+
}
43+
};
44+
45+
const handleDelete = (e: React.MouseEvent) => {
46+
e.stopPropagation();
47+
onFileDelete(node.data.id);
48+
};
49+
50+
return (
51+
<div
52+
className={`group flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 transition-all duration-200 ${
53+
isActive
54+
? "border-l-2 border-green-800 bg-gray-1200 text-white shadow-sm"
55+
: isHovered
56+
? "bg-gray-1100 text-gray-200"
57+
: "text-gray-400 hover:text-gray-200"
58+
}`}
59+
onClick={handleClick}
60+
onKeyDown={(e) => {
61+
if (e.key === "Enter" || e.key === " ") {
62+
e.preventDefault();
63+
handleClick();
64+
}
65+
}}
66+
onMouseEnter={() => setIsHovered(true)}
67+
onMouseLeave={() => setIsHovered(false)}
68+
role="button"
69+
style={style}
70+
tabIndex={0}
71+
>
72+
<div className="flex min-w-0 flex-1 items-center gap-2">
73+
{node.data.isFolder ? (
74+
<>
75+
<IconSvg
76+
className={`size-4 shrink-0 transition-transform duration-200 ${
77+
node.isOpen ? "rotate-0" : "-rotate-90"
78+
} ${isActive ? "fill-green-800" : "fill-gray-400 group-hover:fill-green-800"}`}
79+
src={ChevronDownIcon}
80+
/>
81+
<svg
82+
className={`size-4 shrink-0 ${isActive ? "fill-green-800" : "fill-green-500 group-hover:fill-green-800"}`}
83+
fill="currentColor"
84+
viewBox="0 0 24 24"
85+
xmlns="http://www.w3.org/2000/svg"
86+
>
87+
<path d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2h-8l-2-2z" />
88+
</svg>
89+
</>
90+
) : (
91+
<>
92+
<div className="size-4 shrink-0" />
93+
<IconSvg
94+
className={`size-4 shrink-0 ${isActive ? "stroke-green-800" : "text-gray-400"}`}
95+
src={FileIcon}
96+
/>
97+
</>
98+
)}
99+
<span
100+
className={`truncate text-sm font-medium ${isActive ? "text-white" : "text-gray-400"}`}
101+
title={node.data.name}
102+
>
103+
{node.data.name}
104+
</span>
105+
</div>
106+
107+
{!node.data.isFolder ? (
108+
<button
109+
className="flex size-6 shrink-0 items-center justify-center rounded opacity-0 transition-all hover:bg-gray-1250 group-hover:opacity-100"
110+
onClick={handleDelete}
111+
type="button"
112+
>
113+
<IconSvg className="size-4 stroke-gray-400 hover:stroke-red-500" src={TrashIcon} />
114+
</button>
115+
) : null}
116+
</div>
117+
);
118+
};
119+
120+
export const FileTree = ({ data, activeFilePath, onFileClick, onFileDelete, height }: FileTreeProps) => {
121+
return (
122+
<Tree data={data} height={height} indent={12} openByDefault={false} rowHeight={40} width="100%">
123+
{(props) => (
124+
<FileNode
125+
{...props}
126+
activeFilePath={activeFilePath}
127+
onFileClick={onFileClick}
128+
onFileDelete={onFileDelete}
129+
/>
130+
)}
131+
</Tree>
132+
);
133+
};

0 commit comments

Comments
 (0)