@@ -0,0 +1,89 @@
|
||||
import React from "react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { FiFolder } from "react-icons/fi";
|
||||
import { getLanguage } from "../helpers/fileTree";
|
||||
|
||||
interface CodeEditorProps {
|
||||
filePath: string;
|
||||
content: string;
|
||||
onChange: (content: string) => void;
|
||||
}
|
||||
|
||||
export const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
filePath,
|
||||
content,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#1e1e1e",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1 }}>
|
||||
{filePath ? (
|
||||
<Editor
|
||||
height="100%"
|
||||
language={getLanguage(filePath)}
|
||||
value={content}
|
||||
onChange={(value) => onChange(value || "")}
|
||||
theme="vs-dark"
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
fontFamily: "'Cascadia Code', 'Fira Code', monospace",
|
||||
tabSize: 4,
|
||||
wordWrap: "on",
|
||||
lineNumbers: "on",
|
||||
automaticLayout: true,
|
||||
renderWhitespace: "selection",
|
||||
smoothScrolling: true,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "#858585",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "24px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<FiFolder size={64} />
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "18px",
|
||||
marginBottom: "12px",
|
||||
color: "#cccccc",
|
||||
}}
|
||||
>
|
||||
Welcome to Web VS Code
|
||||
</div>
|
||||
<div style={{ fontSize: "13px", marginBottom: "8px" }}>
|
||||
Right-click on a folder to create files
|
||||
</div>
|
||||
<div style={{ fontSize: "12px", color: "#0e639c" }}>
|
||||
Or right-click anywhere in the explorer
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { FiFile, FiFolder, FiEdit3, FiTrash2 } from "react-icons/fi";
|
||||
|
||||
const MenuItem: React.FC<{
|
||||
onClick: () => void;
|
||||
danger?: boolean;
|
||||
children: React.ReactNode;
|
||||
}> = ({ onClick, danger, children }) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
color: danger ? "#f48771" : "#cccccc",
|
||||
fontSize: "13px",
|
||||
transition: "background-color 0.1s",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface ContextMenuProps {
|
||||
x: number;
|
||||
y: number;
|
||||
onClose: () => void;
|
||||
onNewFile: () => void;
|
||||
onNewFolder: () => void;
|
||||
onRename: () => void;
|
||||
onDelete: () => void;
|
||||
hasNode: boolean;
|
||||
}
|
||||
|
||||
export const ContextMenu: React.FC<ContextMenuProps> = ({
|
||||
x,
|
||||
y,
|
||||
onClose,
|
||||
onNewFile,
|
||||
onNewFolder,
|
||||
onRename,
|
||||
onDelete,
|
||||
hasNode,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handleClick = () => onClose();
|
||||
document.addEventListener("click", handleClick);
|
||||
return () => document.removeEventListener("click", handleClick);
|
||||
}, [onClose]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: y,
|
||||
left: x,
|
||||
backgroundColor: "#252526",
|
||||
border: "1px solid #3e3e42",
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
|
||||
zIndex: 1000,
|
||||
minWidth: "180px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={onNewFile}>
|
||||
<FiFile /> New File
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onNewFolder}>
|
||||
<FiFolder /> New Folder
|
||||
</MenuItem>
|
||||
{hasNode && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
height: "1px",
|
||||
backgroundColor: "#3e3e42",
|
||||
margin: "4px 0",
|
||||
}}
|
||||
/>
|
||||
<MenuItem onClick={onRename}>
|
||||
<FiEdit3 /> Rename
|
||||
</MenuItem>
|
||||
<MenuItem onClick={onDelete} danger>
|
||||
<FiTrash2 /> Delete
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,318 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FiSearch, FiFile, FiFolder, FiMinus } from "react-icons/fi";
|
||||
import { GoKebabHorizontal } from "react-icons/go";
|
||||
import { MdClose, MdAdd } from "react-icons/md";
|
||||
import { FileTreeItem } from "./FileTreeItem";
|
||||
import { ContextMenu } from "./ContextMenu";
|
||||
import { InputDialog } from "./InputDialog";
|
||||
import { filterTree, collectPathsToExpand } from "../helpers/fileTree";
|
||||
import { useIDEStore } from "../store/useIDEStore";
|
||||
import type { FileNode } from "../types";
|
||||
|
||||
interface FileExplorerProps {
|
||||
files: FileNode;
|
||||
onDeleteRoot: () => void;
|
||||
}
|
||||
|
||||
export const FileExplorer: React.FC<FileExplorerProps> = ({
|
||||
files,
|
||||
onDeleteRoot,
|
||||
}) => {
|
||||
const store = useIDEStore();
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
|
||||
const handleEmptyContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
store.setContextMenu({ x: e.clientX, y: e.clientY, node: null });
|
||||
};
|
||||
|
||||
const handleNodeContextMenu = (e: React.MouseEvent, node: FileNode) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
store.setContextMenu({ x: e.clientX, y: e.clientY, node });
|
||||
};
|
||||
|
||||
const filteredFiles = store.searchQuery
|
||||
? filterTree(files, store.searchQuery)
|
||||
: files;
|
||||
|
||||
useEffect(() => {
|
||||
if (store.searchQuery && files) {
|
||||
const pathsToExpand = collectPathsToExpand(files, store.searchQuery);
|
||||
if (pathsToExpand.size > 0) {
|
||||
store.autoExpandPaths(pathsToExpand);
|
||||
}
|
||||
}
|
||||
}, [store.searchQuery, files, store.autoExpandPaths]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#252526",
|
||||
}}
|
||||
onContextMenu={handleEmptyContextMenu}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
padding: "0 8px",
|
||||
height: "35px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
borderBottom: "1px solid #3e3e42",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
color: "#bbbbbb",
|
||||
fontWeight: 500,
|
||||
fontSize: "11px",
|
||||
letterSpacing: "0.8px",
|
||||
}}
|
||||
>
|
||||
EXPLORER
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: "2px", alignItems: "center" }}>
|
||||
<button
|
||||
onClick={() => setShowSearch(!showSearch)}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: showSearch ? "#cccccc" : "#858585",
|
||||
cursor: "pointer",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.1s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
title="Search in files"
|
||||
>
|
||||
<FiSearch size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={store.collapseAllFolders}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#858585",
|
||||
cursor: "pointer",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.1s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
e.currentTarget.style.color = "#cccccc";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
e.currentTarget.style.color = "#858585";
|
||||
}}
|
||||
title="Collapse All"
|
||||
>
|
||||
<FiMinus size={14} />
|
||||
</button>
|
||||
<button
|
||||
onClick={store.expandAllFolders}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#858585",
|
||||
cursor: "pointer",
|
||||
padding: "4px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
transition: "all 0.1s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
e.currentTarget.style.color = "#cccccc";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
e.currentTarget.style.color = "#858585";
|
||||
}}
|
||||
title="Expand All"
|
||||
>
|
||||
<GoKebabHorizontal size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: "6px 12px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
borderBottom: "1px solid #3e3e42",
|
||||
}}
|
||||
>
|
||||
<FiFolder size={14} color="#858585" />
|
||||
<span
|
||||
style={{
|
||||
color: "#cccccc",
|
||||
fontWeight: 600,
|
||||
fontSize: "11px",
|
||||
letterSpacing: "0.3px",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{files.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showSearch && (
|
||||
<div style={{ padding: "6px 8px", borderBottom: "1px solid #3e3e42" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#3c3c3c",
|
||||
border: store.searchQuery
|
||||
? "1px solid #007acc"
|
||||
: "1px solid transparent",
|
||||
borderRadius: "4px",
|
||||
padding: "0 6px",
|
||||
transition: "border-color 0.1s",
|
||||
}}
|
||||
>
|
||||
<FiSearch size={13} color="#858585" />
|
||||
<input
|
||||
type="text"
|
||||
value={store.searchQuery}
|
||||
onChange={(e) => store.setSearchQuery(e.target.value)}
|
||||
placeholder="Search..."
|
||||
autoFocus
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: "5px 6px",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
color: "#cccccc",
|
||||
fontSize: "12px",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
{store.searchQuery && (
|
||||
<button
|
||||
onClick={() => store.setSearchQuery("")}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#858585",
|
||||
cursor: "pointer",
|
||||
padding: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<MdClose size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
{filteredFiles ? (
|
||||
<FileTreeItem
|
||||
node={filteredFiles}
|
||||
level={0}
|
||||
onFileSelect={store.selectFile}
|
||||
selectedFile={store.activeFile?.path || null}
|
||||
onContextMenu={handleNodeContextMenu}
|
||||
expandedFolders={store.expandedFolders}
|
||||
onToggleFolder={store.toggleFolder}
|
||||
onDelete={store.handleDeleteNode}
|
||||
isRoot
|
||||
searchQuery={store.searchQuery}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
padding: "16px",
|
||||
color: "#858585",
|
||||
fontSize: "13px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{store.contextMenu && (
|
||||
<ContextMenu
|
||||
x={store.contextMenu.x}
|
||||
y={store.contextMenu.y}
|
||||
onClose={() => store.setContextMenu(null)}
|
||||
onNewFile={() => {
|
||||
store.setDialog({
|
||||
type: "newFile",
|
||||
node: store.contextMenu?.node || null,
|
||||
});
|
||||
store.setContextMenu(null);
|
||||
}}
|
||||
onNewFolder={() => {
|
||||
store.setDialog({
|
||||
type: "newFolder",
|
||||
node: store.contextMenu?.node || null,
|
||||
});
|
||||
store.setContextMenu(null);
|
||||
}}
|
||||
onRename={() => {
|
||||
store.setDialog({
|
||||
type: "rename",
|
||||
node: store.contextMenu?.node || null,
|
||||
});
|
||||
store.setContextMenu(null);
|
||||
}}
|
||||
onDelete={() => {
|
||||
if (store.contextMenu?.node) {
|
||||
store.handleDeleteNode(store.contextMenu.node);
|
||||
}
|
||||
store.setContextMenu(null);
|
||||
}}
|
||||
hasNode={!!store.contextMenu.node}
|
||||
/>
|
||||
)}
|
||||
|
||||
{store.dialog && (
|
||||
<InputDialog
|
||||
title={
|
||||
store.dialog.type === "newFile"
|
||||
? "New File"
|
||||
: store.dialog.type === "newFolder"
|
||||
? "New Folder"
|
||||
: "Rename"
|
||||
}
|
||||
initialValue={
|
||||
store.dialog.type === "rename" && store.dialog.node
|
||||
? store.dialog.node.name
|
||||
: ""
|
||||
}
|
||||
onConfirm={store.handleDialogConfirm}
|
||||
onCancel={() => store.setDialog(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,169 @@
|
||||
import React, { useState } from "react";
|
||||
import { FiChevronRight, FiChevronDown, FiTrash2 } from "react-icons/fi";
|
||||
import { GoFile } from "react-icons/go";
|
||||
import type { FileNode } from "../types";
|
||||
|
||||
interface FileTreeItemProps {
|
||||
node: FileNode;
|
||||
level: number;
|
||||
onFileSelect: (node: FileNode) => void;
|
||||
selectedFile: string | null;
|
||||
onContextMenu: (e: React.MouseEvent, node: FileNode) => void;
|
||||
expandedFolders: Set<string>;
|
||||
onToggleFolder: (path: string) => void;
|
||||
onDelete: (node: FileNode) => void;
|
||||
isRoot?: boolean;
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
export const FileTreeItem: React.FC<FileTreeItemProps> = ({
|
||||
node,
|
||||
level,
|
||||
onFileSelect,
|
||||
selectedFile,
|
||||
onContextMenu,
|
||||
expandedFolders,
|
||||
onToggleFolder,
|
||||
onDelete,
|
||||
isRoot,
|
||||
searchQuery,
|
||||
}) => {
|
||||
const isFolder = node.type === "folder";
|
||||
const isSelected = selectedFile === node.path && !isFolder;
|
||||
const isExpanded = expandedFolders.has(node.path || node.name);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const handleClick = () => {
|
||||
if (isFolder) {
|
||||
onToggleFolder(node.path || node.name);
|
||||
} else {
|
||||
onFileSelect(node);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onDelete(node);
|
||||
};
|
||||
|
||||
const highlightText = (text: string, query: string) => {
|
||||
if (!query) return text;
|
||||
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
||||
if (idx === -1) return text;
|
||||
return (
|
||||
<>
|
||||
{text.slice(0, idx)}
|
||||
<span style={{ backgroundColor: "#613214", color: "#f9f9a4" }}>
|
||||
{text.slice(idx, idx + query.length)}
|
||||
</span>
|
||||
{text.slice(idx + query.length)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onContextMenu={(e) => onContextMenu(e, node)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
style={{
|
||||
paddingLeft: isRoot ? "8px" : `${level * 16 + 8}px`,
|
||||
paddingTop: "4px",
|
||||
paddingBottom: "4px",
|
||||
cursor: "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
backgroundColor: isSelected ? "#094771" : "transparent",
|
||||
color: isSelected ? "#fff" : "#cccccc",
|
||||
fontSize: "13px",
|
||||
transition: "background-color 0.1s",
|
||||
userSelect: "none",
|
||||
minHeight: "28px",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
width: "16px",
|
||||
textAlign: "center",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{isFolder ? (
|
||||
isExpanded ? (
|
||||
<FiChevronDown />
|
||||
) : (
|
||||
<FiChevronRight />
|
||||
)
|
||||
) : (
|
||||
<GoFile />
|
||||
)}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{searchQuery ? highlightText(node.name, searchQuery) : node.name}
|
||||
</span>
|
||||
{hovered && !isRoot && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
title={`Delete ${node.name}`}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#858585",
|
||||
cursor: "pointer",
|
||||
padding: "2px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: "3px",
|
||||
flexShrink: 0,
|
||||
width: "20px",
|
||||
height: "20px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = "#f48771";
|
||||
e.currentTarget.style.backgroundColor = "#3e3e42";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = "#858585";
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
<FiTrash2 size={13} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isFolder && isExpanded && node.children && (
|
||||
<div>
|
||||
{node.children.map((child, idx) => (
|
||||
<FileTreeItem
|
||||
key={idx}
|
||||
node={child}
|
||||
level={level + 1}
|
||||
onFileSelect={onFileSelect}
|
||||
selectedFile={selectedFile}
|
||||
onContextMenu={onContextMenu}
|
||||
expandedFolders={expandedFolders}
|
||||
onToggleFolder={onToggleFolder}
|
||||
onDelete={onDelete}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
|
||||
interface InputDialogProps {
|
||||
title: string;
|
||||
initialValue?: string;
|
||||
onConfirm: (value: string) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export const InputDialog: React.FC<InputDialogProps> = ({
|
||||
title,
|
||||
initialValue = "",
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2000,
|
||||
}}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#2d2d30",
|
||||
borderRadius: "8px",
|
||||
padding: "24px",
|
||||
minWidth: "320px",
|
||||
border: "1px solid #3e3e42",
|
||||
boxShadow: "0 8px 24px rgba(0,0,0,0.4)",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3
|
||||
style={{
|
||||
margin: "0 0 8px 0",
|
||||
color: "#fff",
|
||||
fontSize: "16px",
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<p style={{ margin: "0 0 16px 0", color: "#858585", fontSize: "12px" }}>
|
||||
Enter a new name
|
||||
</p>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" && value.trim() && onConfirm(value.trim())
|
||||
}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "10px",
|
||||
backgroundColor: "#3c3c3c",
|
||||
border: "1px solid #3e3e42",
|
||||
borderRadius: "6px",
|
||||
color: "#ccc",
|
||||
fontSize: "14px",
|
||||
marginBottom: "20px",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{ display: "flex", gap: "12px", justifyContent: "flex-end" }}
|
||||
>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
style={{
|
||||
padding: "6px 16px",
|
||||
backgroundColor: "transparent",
|
||||
border: "1px solid #0e639c",
|
||||
borderRadius: "4px",
|
||||
color: "#0e639c",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => value.trim() && onConfirm(value.trim())}
|
||||
style={{
|
||||
padding: "6px 16px",
|
||||
backgroundColor: "#0e639c",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
color: "#fff",
|
||||
cursor: "pointer",
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from "react";
|
||||
import { FiGitBranch, FiCheckCircle, FiAlertCircle } from "react-icons/fi";
|
||||
import type { FileNode } from "../types";
|
||||
|
||||
interface StatusBarProps {
|
||||
activeFile: FileNode | null;
|
||||
}
|
||||
|
||||
export const StatusBar: React.FC<StatusBarProps> = ({ activeFile }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "22px",
|
||||
backgroundColor: "#007acc",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 12px",
|
||||
fontSize: "12px",
|
||||
color: "#ffffff",
|
||||
userSelect: "none",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", gap: "16px", alignItems: "center" }}>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<FiGitBranch size={12} /> main
|
||||
</span>
|
||||
<span style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FiCheckCircle size={12} /> 0 <FiAlertCircle size={12} /> 0
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "16px", alignItems: "center" }}>
|
||||
{activeFile && (
|
||||
<span>
|
||||
Ln 1, Col 1 | Spaces: 4 | UTF-8 |{" "}
|
||||
{activeFile.path?.split(".").pop()?.toUpperCase() || "TXT"}
|
||||
</span>
|
||||
)}
|
||||
<span>Web VS Code</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
import React, { useState } from "react";
|
||||
import { GoFile } from "react-icons/go";
|
||||
import { MdClose } from "react-icons/md";
|
||||
import type { FileNode } from "../types";
|
||||
|
||||
interface TabBarProps {
|
||||
openFiles: FileNode[];
|
||||
activeFile: FileNode | null;
|
||||
onSelectFile: (file: FileNode) => void;
|
||||
onCloseFile: (file: FileNode) => void;
|
||||
onCloseAll: () => void;
|
||||
onCloseOthers: (file: FileNode) => void;
|
||||
}
|
||||
|
||||
export const TabBar: React.FC<TabBarProps> = ({
|
||||
openFiles,
|
||||
activeFile,
|
||||
onSelectFile,
|
||||
onCloseFile,
|
||||
onCloseAll,
|
||||
onCloseOthers,
|
||||
}) => {
|
||||
const [showContextMenu, setShowContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
file: FileNode;
|
||||
} | null>(null);
|
||||
|
||||
const handleContextMenu = (e: React.MouseEvent, file: FileNode) => {
|
||||
e.preventDefault();
|
||||
setShowContextMenu({ x: e.clientX, y: e.clientY, file });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#1e1e1e",
|
||||
borderBottom: "1px solid #3e3e42",
|
||||
overflowX: "auto",
|
||||
minHeight: "40px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
padding: "0 12px",
|
||||
gap: "8px",
|
||||
borderRight: "1px solid #3e3e42",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={onCloseAll}
|
||||
style={{
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
color: "#cccccc",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
padding: "6px 8px",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "6px",
|
||||
transition: "all 0.1s",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
title="Close All"
|
||||
>
|
||||
<MdClose size={14} />
|
||||
<span style={{ fontSize: "11px" }}>Close All</span>
|
||||
</button>
|
||||
</div>
|
||||
{openFiles.map((file) => (
|
||||
<div
|
||||
key={file.path}
|
||||
onClick={() => onSelectFile(file)}
|
||||
onContextMenu={(e) => handleContextMenu(e, file)}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px 16px",
|
||||
backgroundColor:
|
||||
activeFile?.path === file.path ? "#1e1e1e" : "#2d2d30",
|
||||
color: activeFile?.path === file.path ? "#fff" : "#cccccc",
|
||||
borderRight: "1px solid #3e3e42",
|
||||
cursor: "pointer",
|
||||
fontSize: "13px",
|
||||
gap: "10px",
|
||||
whiteSpace: "nowrap",
|
||||
transition: "all 0.1s",
|
||||
borderTop:
|
||||
activeFile?.path === file.path
|
||||
? "2px solid #0e639c"
|
||||
: "2px solid transparent",
|
||||
}}
|
||||
>
|
||||
<GoFile />
|
||||
<span>{file.name}</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCloseFile(file);
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#858585",
|
||||
cursor: "pointer",
|
||||
fontSize: "16px",
|
||||
padding: "0 4px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.color = "#fff";
|
||||
e.currentTarget.style.backgroundColor = "#3e3e42";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.color = "#858585";
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
<MdClose size={14} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{showContextMenu && (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: showContextMenu.y,
|
||||
left: showContextMenu.x,
|
||||
backgroundColor: "#252526",
|
||||
border: "1px solid #3e3e42",
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.4)",
|
||||
zIndex: 1000,
|
||||
minWidth: "160px",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
onCloseOthers(showContextMenu.file);
|
||||
setShowContextMenu(null);
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
color: "#cccccc",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
Close Others
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
onCloseAll();
|
||||
setShowContextMenu(null);
|
||||
}}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
cursor: "pointer",
|
||||
color: "#cccccc",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "#2a2d2e";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.backgroundColor = "transparent";
|
||||
}}
|
||||
>
|
||||
Close All
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { FiGitBranch, FiCheckCircle } from "react-icons/fi";
|
||||
|
||||
export const TitleBar: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "32px",
|
||||
backgroundColor: "#2d2d30",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "0 12px",
|
||||
borderBottom: "1px solid #3e3e42",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "12px" }}>
|
||||
<div style={{ display: "flex", gap: "8px" }}>
|
||||
<div
|
||||
style={{
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#ed6a5e",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#f5bd4f",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#61c454",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span style={{ color: "#cccccc", fontSize: "12px", fontWeight: 500 }}>
|
||||
Web VS Code
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||
<FiGitBranch size={12} color="#858585" />
|
||||
<span style={{ color: "#858585", fontSize: "11px" }}>main</span>
|
||||
<FiCheckCircle size={12} color="#61c454" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export { ContextMenu } from "./ContextMenu";
|
||||
export { InputDialog } from "./InputDialog";
|
||||
export { FileTreeItem } from "./FileTreeItem";
|
||||
export { FileExplorer } from "./FileExplorer";
|
||||
export { TabBar } from "./TabBar";
|
||||
export { CodeEditor } from "./CodeEditor";
|
||||
export { TitleBar } from "./TitleBar";
|
||||
export { StatusBar } from "./StatusBar";
|
||||
Reference in New Issue
Block a user