支持树形资源管理器
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||||
|
import { FileTree } from './FileTree';
|
||||||
|
import { ResizablePanel } from './ResizablePanel';
|
||||||
import '../styles/AssetBrowser.css';
|
import '../styles/AssetBrowser.css';
|
||||||
|
|
||||||
interface AssetItem {
|
interface AssetItem {
|
||||||
@@ -14,10 +16,13 @@ interface AssetBrowserProps {
|
|||||||
locale: string;
|
locale: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ViewMode = 'tree-split' | 'tree-only';
|
||||||
|
|
||||||
export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
||||||
const [currentPath, setCurrentPath] = useState<string>('');
|
const [viewMode, setViewMode] = useState<ViewMode>('tree-split');
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||||
const [assets, setAssets] = useState<AssetItem[]>([]);
|
const [assets, setAssets] = useState<AssetItem[]>([]);
|
||||||
const [selectedAsset, setSelectedAsset] = useState<string | null>(null);
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
@@ -26,22 +31,26 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
noProject: 'No project loaded',
|
noProject: 'No project loaded',
|
||||||
loading: 'Loading...',
|
loading: 'Loading...',
|
||||||
empty: 'No assets found',
|
empty: 'No assets found',
|
||||||
|
search: 'Search...',
|
||||||
|
viewTreeSplit: 'Tree + List',
|
||||||
|
viewTreeOnly: 'Tree Only',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
type: 'Type',
|
type: 'Type',
|
||||||
file: 'File',
|
file: 'File',
|
||||||
folder: 'Folder',
|
folder: 'Folder'
|
||||||
backToParent: 'Back to parent folder'
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: '资产',
|
title: '资产',
|
||||||
noProject: '没有加载项目',
|
noProject: '没有加载项目',
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
empty: '没有找到资产',
|
empty: '没有找到资产',
|
||||||
|
search: '搜索...',
|
||||||
|
viewTreeSplit: '树形+列表',
|
||||||
|
viewTreeOnly: '纯树形',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
file: '文件',
|
file: '文件',
|
||||||
folder: '文件夹',
|
folder: '文件夹'
|
||||||
backToParent: '返回上一级'
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,13 +58,14 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectPath) {
|
if (projectPath) {
|
||||||
setCurrentPath(projectPath);
|
if (viewMode === 'tree-split') {
|
||||||
loadAssets(projectPath);
|
loadAssets(projectPath);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setAssets([]);
|
setAssets([]);
|
||||||
setCurrentPath('');
|
setSelectedPath(null);
|
||||||
}
|
}
|
||||||
}, [projectPath]);
|
}, [projectPath, viewMode]);
|
||||||
|
|
||||||
const loadAssets = async (path: string) => {
|
const loadAssets = async (path: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -83,28 +93,26 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAssetClick = (asset: AssetItem) => {
|
const handleTreeSelect = (path: string) => {
|
||||||
setSelectedAsset(asset.path);
|
setSelectedPath(path);
|
||||||
if (asset.type === 'folder') {
|
if (viewMode === 'tree-split') {
|
||||||
setCurrentPath(asset.path);
|
loadAssets(path);
|
||||||
loadAssets(asset.path);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAssetClick = (asset: AssetItem) => {
|
||||||
|
setSelectedPath(asset.path);
|
||||||
|
};
|
||||||
|
|
||||||
const handleAssetDoubleClick = (asset: AssetItem) => {
|
const handleAssetDoubleClick = (asset: AssetItem) => {
|
||||||
console.log('Open asset:', asset);
|
console.log('Open asset:', asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleBackToParent = () => {
|
const filteredAssets = searchQuery
|
||||||
if (!currentPath || !projectPath) return;
|
? assets.filter(asset =>
|
||||||
if (currentPath === projectPath) return;
|
asset.type === 'file' && asset.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
const parentPath = currentPath.split(/[\\/]/).slice(0, -1).join(currentPath.includes('\\') ? '\\' : '/');
|
: assets.filter(asset => asset.type === 'file');
|
||||||
setCurrentPath(parentPath);
|
|
||||||
loadAssets(parentPath);
|
|
||||||
};
|
|
||||||
|
|
||||||
const canGoBack = currentPath && projectPath && currentPath !== projectPath;
|
|
||||||
|
|
||||||
const getFileIcon = (extension?: string) => {
|
const getFileIcon = (extension?: string) => {
|
||||||
switch (extension?.toLowerCase()) {
|
switch (extension?.toLowerCase()) {
|
||||||
@@ -147,12 +155,6 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFolderIcon = () => (
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" className="asset-icon folder">
|
|
||||||
<path d="M3 7V17C3 18.1046 3.89543 19 5 19H19C20.1046 19 21 18.1046 21 17V9C21 7.89543 20.1046 7 19 7H13L11 5H5C3.89543 5 3 5.89543 3 7Z" strokeWidth="2"/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
return (
|
return (
|
||||||
<div className="asset-browser">
|
<div className="asset-browser">
|
||||||
@@ -166,69 +168,114 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const renderListView = () => (
|
||||||
<div className="asset-browser">
|
<div className="asset-browser-list">
|
||||||
<div className="asset-browser-header">
|
<div className="asset-browser-toolbar">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
<input
|
||||||
<h3 style={{ margin: 0 }}>{t.title}</h3>
|
type="text"
|
||||||
{canGoBack && (
|
className="asset-search"
|
||||||
<button
|
placeholder={t.search}
|
||||||
onClick={handleBackToParent}
|
value={searchQuery}
|
||||||
className="back-button"
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
title={t.backToParent}
|
/>
|
||||||
style={{
|
|
||||||
padding: '4px 8px',
|
|
||||||
background: '#0e639c',
|
|
||||||
color: '#fff',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '3px',
|
|
||||||
cursor: 'pointer',
|
|
||||||
fontSize: '12px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: '4px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
||||||
</svg>
|
|
||||||
↑
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="asset-path">{currentPath}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="asset-browser-loading">
|
<div className="asset-browser-loading">
|
||||||
<p>{t.loading}</p>
|
<p>{t.loading}</p>
|
||||||
</div>
|
</div>
|
||||||
) : assets.length === 0 ? (
|
) : filteredAssets.length === 0 ? (
|
||||||
<div className="asset-browser-empty">
|
<div className="asset-browser-empty">
|
||||||
<p>{t.empty}</p>
|
<p>{t.empty}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="asset-browser-content">
|
|
||||||
<div className="asset-list">
|
<div className="asset-list">
|
||||||
{assets.map((asset, index) => (
|
{filteredAssets.map((asset, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`asset-item ${selectedAsset === asset.path ? 'selected' : ''}`}
|
className={`asset-item ${selectedPath === asset.path ? 'selected' : ''}`}
|
||||||
onClick={() => handleAssetClick(asset)}
|
onClick={() => handleAssetClick(asset)}
|
||||||
onDoubleClick={() => handleAssetDoubleClick(asset)}
|
onDoubleClick={() => handleAssetDoubleClick(asset)}
|
||||||
>
|
>
|
||||||
{asset.type === 'folder' ? getFolderIcon() : getFileIcon(asset.extension)}
|
{getFileIcon(asset.extension)}
|
||||||
<div className="asset-name" title={asset.name}>
|
<div className="asset-name" title={asset.name}>
|
||||||
{asset.name}
|
{asset.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="asset-type">
|
<div className="asset-type">
|
||||||
{asset.type === 'folder' ? t.folder : asset.extension || t.file}
|
{asset.extension || t.file}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="asset-browser">
|
||||||
|
<div className="asset-browser-header">
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||||
|
<h3 style={{ margin: 0 }}>{t.title}</h3>
|
||||||
|
<div className="view-mode-buttons">
|
||||||
|
<button
|
||||||
|
className={`view-mode-btn ${viewMode === 'tree-split' ? 'active' : ''}`}
|
||||||
|
onClick={() => setViewMode('tree-split')}
|
||||||
|
title={t.viewTreeSplit}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="3" y="3" width="7" height="18"/>
|
||||||
|
<rect x="14" y="3" width="7" height="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`view-mode-btn ${viewMode === 'tree-only' ? 'active' : ''}`}
|
||||||
|
onClick={() => setViewMode('tree-only')}
|
||||||
|
title={t.viewTreeOnly}
|
||||||
|
>
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<rect x="3" y="3" width="18" height="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="asset-browser-content">
|
||||||
|
{viewMode === 'tree-only' ? (
|
||||||
|
<div className="asset-browser-tree-only">
|
||||||
|
<div className="asset-browser-toolbar">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="asset-search"
|
||||||
|
placeholder={t.search}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FileTree
|
||||||
|
rootPath={projectPath}
|
||||||
|
onSelectFile={handleTreeSelect}
|
||||||
|
selectedPath={selectedPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ResizablePanel
|
||||||
|
direction="horizontal"
|
||||||
|
defaultSize={200}
|
||||||
|
minSize={150}
|
||||||
|
maxSize={400}
|
||||||
|
leftOrTop={
|
||||||
|
<div className="asset-browser-tree">
|
||||||
|
<FileTree
|
||||||
|
rootPath={projectPath}
|
||||||
|
onSelectFile={handleTreeSelect}
|
||||||
|
selectedPath={selectedPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightOrBottom={renderListView()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
175
packages/editor-app/src/components/FileTree.tsx
Normal file
175
packages/editor-app/src/components/FileTree.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||||
|
import '../styles/FileTree.css';
|
||||||
|
|
||||||
|
interface TreeNode {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
type: 'file' | 'folder';
|
||||||
|
extension?: string;
|
||||||
|
children?: TreeNode[];
|
||||||
|
expanded?: boolean;
|
||||||
|
loaded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileTreeProps {
|
||||||
|
rootPath: string | null;
|
||||||
|
onSelectFile?: (path: string) => void;
|
||||||
|
selectedPath?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps) {
|
||||||
|
const [tree, setTree] = useState<TreeNode[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (rootPath) {
|
||||||
|
loadRootDirectory(rootPath);
|
||||||
|
} else {
|
||||||
|
setTree([]);
|
||||||
|
}
|
||||||
|
}, [rootPath]);
|
||||||
|
|
||||||
|
const loadRootDirectory = async (path: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const entries = await TauriAPI.listDirectory(path);
|
||||||
|
const nodes = entriesToNodes(entries);
|
||||||
|
setTree(nodes);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load directory:', error);
|
||||||
|
setTree([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const entriesToNodes = (entries: DirectoryEntry[]): TreeNode[] => {
|
||||||
|
return entries.map(entry => ({
|
||||||
|
name: entry.name,
|
||||||
|
path: entry.path,
|
||||||
|
type: entry.is_dir ? 'folder' : 'file',
|
||||||
|
extension: !entry.is_dir && entry.name.includes('.')
|
||||||
|
? entry.name.split('.').pop()
|
||||||
|
: undefined,
|
||||||
|
children: entry.is_dir ? [] : undefined,
|
||||||
|
expanded: false,
|
||||||
|
loaded: false
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadChildren = async (node: TreeNode): Promise<TreeNode[]> => {
|
||||||
|
try {
|
||||||
|
const entries = await TauriAPI.listDirectory(node.path);
|
||||||
|
return entriesToNodes(entries);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load children:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleNode = async (nodePath: string) => {
|
||||||
|
const updateTree = async (nodes: TreeNode[]): Promise<TreeNode[]> => {
|
||||||
|
const newNodes: TreeNode[] = [];
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.path === nodePath && node.type === 'folder') {
|
||||||
|
if (!node.loaded) {
|
||||||
|
const children = await loadChildren(node);
|
||||||
|
newNodes.push({
|
||||||
|
...node,
|
||||||
|
expanded: true,
|
||||||
|
loaded: true,
|
||||||
|
children
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newNodes.push({
|
||||||
|
...node,
|
||||||
|
expanded: !node.expanded
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (node.children) {
|
||||||
|
newNodes.push({
|
||||||
|
...node,
|
||||||
|
children: await updateTree(node.children)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newNodes.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newNodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const newTree = await updateTree(tree);
|
||||||
|
setTree(newTree);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNodeClick = (node: TreeNode) => {
|
||||||
|
onSelectFile?.(node.path);
|
||||||
|
if (node.type === 'folder') {
|
||||||
|
toggleNode(node.path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFileIcon = (extension?: string) => {
|
||||||
|
switch (extension?.toLowerCase()) {
|
||||||
|
case 'ts':
|
||||||
|
case 'tsx':
|
||||||
|
case 'js':
|
||||||
|
case 'jsx':
|
||||||
|
return '📄';
|
||||||
|
case 'json':
|
||||||
|
return '📋';
|
||||||
|
case 'png':
|
||||||
|
case 'jpg':
|
||||||
|
case 'jpeg':
|
||||||
|
case 'gif':
|
||||||
|
return '🖼️';
|
||||||
|
default:
|
||||||
|
return '📄';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNode = (node: TreeNode, level: number = 0) => {
|
||||||
|
const isSelected = selectedPath === node.path;
|
||||||
|
const indent = level * 16;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={node.path}>
|
||||||
|
<div
|
||||||
|
className={`tree-node ${isSelected ? 'selected' : ''}`}
|
||||||
|
style={{ paddingLeft: `${indent}px` }}
|
||||||
|
onClick={() => handleNodeClick(node)}
|
||||||
|
>
|
||||||
|
{node.type === 'folder' && (
|
||||||
|
<span className="tree-arrow">
|
||||||
|
{node.expanded ? '▼' : '▶'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="tree-icon">
|
||||||
|
{node.type === 'folder' ? '📁' : getFileIcon(node.extension)}
|
||||||
|
</span>
|
||||||
|
<span className="tree-label">{node.name}</span>
|
||||||
|
</div>
|
||||||
|
{node.type === 'folder' && node.expanded && node.children && (
|
||||||
|
<div className="tree-children">
|
||||||
|
{node.children.map(child => renderNode(child, level + 1))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="file-tree loading">Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rootPath || tree.length === 0) {
|
||||||
|
return <div className="file-tree empty">No files</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="file-tree">
|
||||||
|
{tree.map(node => renderNode(node))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,24 +13,87 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-header h3 {
|
.asset-browser-header h3 {
|
||||||
margin: 0 0 8px 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #cccccc;
|
color: #cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-path {
|
.view-mode-buttons {
|
||||||
font-size: 11px;
|
display: flex;
|
||||||
color: #858585;
|
gap: 4px;
|
||||||
overflow: hidden;
|
}
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
.view-mode-btn {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3e3e3e;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #cccccc;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn:hover {
|
||||||
|
background: #2a2d2e;
|
||||||
|
border-color: #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-mode-btn.active {
|
||||||
|
background: #0e639c;
|
||||||
|
border-color: #0e639c;
|
||||||
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-content {
|
.asset-browser-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
padding: 10px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-browser-tree,
|
||||||
|
.asset-browser-tree-only {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-browser-list {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-browser-toolbar {
|
||||||
|
padding: 8px;
|
||||||
|
background: #252526;
|
||||||
|
border-bottom: 1px solid #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
border: 1px solid #3e3e3e;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-search:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
background: #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-search::placeholder {
|
||||||
|
color: #858585;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-loading,
|
.asset-browser-loading,
|
||||||
@@ -44,9 +107,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.asset-list {
|
.asset-list {
|
||||||
|
flex: 1;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
align-content: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-item {
|
.asset-item {
|
||||||
@@ -101,20 +168,33 @@
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-content::-webkit-scrollbar {
|
.asset-browser-list,
|
||||||
|
.asset-list {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-browser-list::-webkit-scrollbar,
|
||||||
|
.asset-list::-webkit-scrollbar,
|
||||||
|
.file-tree::-webkit-scrollbar {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-content::-webkit-scrollbar-track {
|
.asset-browser-list::-webkit-scrollbar-track,
|
||||||
|
.asset-list::-webkit-scrollbar-track,
|
||||||
|
.file-tree::-webkit-scrollbar-track {
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-content::-webkit-scrollbar-thumb {
|
.asset-browser-list::-webkit-scrollbar-thumb,
|
||||||
|
.asset-list::-webkit-scrollbar-thumb,
|
||||||
|
.file-tree::-webkit-scrollbar-thumb {
|
||||||
background: #424242;
|
background: #424242;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-browser-content::-webkit-scrollbar-thumb:hover {
|
.asset-browser-list::-webkit-scrollbar-thumb:hover,
|
||||||
|
.asset-list::-webkit-scrollbar-thumb:hover,
|
||||||
|
.file-tree::-webkit-scrollbar-thumb:hover {
|
||||||
background: #4e4e4e;
|
background: #4e4e4e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
78
packages/editor-app/src/styles/FileTree.css
Normal file
78
packages/editor-app/src/styles/FileTree.css
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
.file-tree {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #cccccc;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar-thumb {
|
||||||
|
background: #424242;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #4e4e4e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree.loading,
|
||||||
|
.file-tree.empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #858585;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node:hover {
|
||||||
|
background: #2a2d2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node.selected {
|
||||||
|
background: #37373d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-arrow {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-children {
|
||||||
|
/* Children are indented via inline style */
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user