Dock系统,支持Tab和拖放
This commit is contained in:
@@ -46,7 +46,7 @@ async fn open_project_dialog(app: AppHandle) -> Result<Option<String>, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn scan_directory(path: String, pattern: String) -> Result<Vec<String>, String> {
|
fn scan_directory(path: String, pattern: String) -> Result<Vec<String>, String> {
|
||||||
use glob::glob;
|
use glob::glob;
|
||||||
use std::path::{Path, MAIN_SEPARATOR};
|
use std::path::Path;
|
||||||
|
|
||||||
let base_path = Path::new(&path);
|
let base_path = Path::new(&path);
|
||||||
if !base_path.exists() {
|
if !base_path.exists() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { StartupPage } from './components/StartupPage';
|
|||||||
import { SceneHierarchy } from './components/SceneHierarchy';
|
import { SceneHierarchy } from './components/SceneHierarchy';
|
||||||
import { EntityInspector } from './components/EntityInspector';
|
import { EntityInspector } from './components/EntityInspector';
|
||||||
import { AssetBrowser } from './components/AssetBrowser';
|
import { AssetBrowser } from './components/AssetBrowser';
|
||||||
import { ResizablePanel } from './components/ResizablePanel';
|
import { DockContainer, DockablePanel } from './components/DockContainer';
|
||||||
import { TauriAPI } from './api/tauri';
|
import { TauriAPI } from './api/tauri';
|
||||||
import { TransformComponent } from './example-components/TransformComponent';
|
import { TransformComponent } from './example-components/TransformComponent';
|
||||||
import { SpriteComponent } from './example-components/SpriteComponent';
|
import { SpriteComponent } from './example-components/SpriteComponent';
|
||||||
@@ -61,6 +61,7 @@ function App() {
|
|||||||
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
const [messageHub, setMessageHub] = useState<MessageHub | null>(null);
|
||||||
const { t, locale, changeLocale } = useLocale();
|
const { t, locale, changeLocale } = useLocale();
|
||||||
const [status, setStatus] = useState(t('header.status.initializing'));
|
const [status, setStatus] = useState(t('header.status.initializing'));
|
||||||
|
const [panels, setPanels] = useState<DockablePanel[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeEditor = async () => {
|
const initializeEditor = async () => {
|
||||||
@@ -199,6 +200,63 @@ function App() {
|
|||||||
changeLocale(newLocale);
|
changeLocale(newLocale);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectLoaded && entityStore && messageHub) {
|
||||||
|
const defaultPanels: DockablePanel[] = [
|
||||||
|
{
|
||||||
|
id: 'scene-hierarchy',
|
||||||
|
title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy',
|
||||||
|
position: 'left',
|
||||||
|
content: <SceneHierarchy entityStore={entityStore} messageHub={messageHub} />,
|
||||||
|
closable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inspector',
|
||||||
|
title: locale === 'zh' ? '检视器' : 'Inspector',
|
||||||
|
position: 'right',
|
||||||
|
content: <EntityInspector entityStore={entityStore} messageHub={messageHub} />,
|
||||||
|
closable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'viewport',
|
||||||
|
title: locale === 'zh' ? '视口' : 'Viewport',
|
||||||
|
position: 'center',
|
||||||
|
content: (
|
||||||
|
<div className="viewport">
|
||||||
|
<h3>{t('viewport.title')}</h3>
|
||||||
|
<p>{t('viewport.placeholder')}</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
closable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'assets',
|
||||||
|
title: locale === 'zh' ? '资产' : 'Assets',
|
||||||
|
position: 'bottom',
|
||||||
|
content: <AssetBrowser projectPath={currentProjectPath} locale={locale} />,
|
||||||
|
closable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'console',
|
||||||
|
title: locale === 'zh' ? '控制台' : 'Console',
|
||||||
|
position: 'bottom',
|
||||||
|
content: (
|
||||||
|
<div style={{ padding: '12px', height: '100%', overflow: 'auto' }}>
|
||||||
|
<h4 style={{ margin: '0 0 8px 0', fontSize: '12px', color: '#ffffff' }}>
|
||||||
|
{locale === 'zh' ? '控制台' : 'Console'}
|
||||||
|
</h4>
|
||||||
|
<p style={{ fontSize: '12px', color: '#858585' }}>
|
||||||
|
{locale === 'zh' ? '控制台输出将显示在这里...' : 'Console output will appear here...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
closable: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
setPanels(defaultPanels);
|
||||||
|
}
|
||||||
|
}, [projectLoaded, entityStore, messageHub, locale, currentProjectPath, t]);
|
||||||
|
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
return (
|
return (
|
||||||
<div className="editor-loading">
|
<div className="editor-loading">
|
||||||
@@ -237,60 +295,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-content">
|
<div className="editor-content">
|
||||||
<ResizablePanel
|
<DockContainer panels={panels} />
|
||||||
direction="horizontal"
|
|
||||||
defaultSize={250}
|
|
||||||
minSize={150}
|
|
||||||
maxSize={400}
|
|
||||||
leftOrTop={
|
|
||||||
<div className="sidebar-left">
|
|
||||||
{entityStore && messageHub ? (
|
|
||||||
<SceneHierarchy entityStore={entityStore} messageHub={messageHub} />
|
|
||||||
) : (
|
|
||||||
<div className="loading">Loading...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
rightOrBottom={
|
|
||||||
<ResizablePanel
|
|
||||||
direction="horizontal"
|
|
||||||
side="right"
|
|
||||||
defaultSize={280}
|
|
||||||
minSize={200}
|
|
||||||
maxSize={500}
|
|
||||||
leftOrTop={
|
|
||||||
<ResizablePanel
|
|
||||||
direction="vertical"
|
|
||||||
defaultSize={200}
|
|
||||||
minSize={100}
|
|
||||||
maxSize={400}
|
|
||||||
leftOrTop={
|
|
||||||
<div className="main-content">
|
|
||||||
<div className="viewport">
|
|
||||||
<h3>{t('viewport.title')}</h3>
|
|
||||||
<p>{t('viewport.placeholder')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
rightOrBottom={
|
|
||||||
<div className="bottom-panel">
|
|
||||||
<AssetBrowser projectPath={currentProjectPath} locale={locale} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
rightOrBottom={
|
|
||||||
<div className="sidebar-right">
|
|
||||||
{entityStore && messageHub ? (
|
|
||||||
<EntityInspector entityStore={entityStore} messageHub={messageHub} />
|
|
||||||
) : (
|
|
||||||
<div className="loading">Loading...</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="editor-footer">
|
<div className="editor-footer">
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
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: '资产',
|
||||||
@@ -39,7 +40,8 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
name: '名称',
|
name: '名称',
|
||||||
type: '类型',
|
type: '类型',
|
||||||
file: '文件',
|
file: '文件',
|
||||||
folder: '文件夹'
|
folder: '文件夹',
|
||||||
|
backToParent: '返回上一级'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -93,6 +95,17 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
console.log('Open asset:', asset);
|
console.log('Open asset:', asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackToParent = () => {
|
||||||
|
if (!currentPath || !projectPath) return;
|
||||||
|
if (currentPath === projectPath) return;
|
||||||
|
|
||||||
|
const parentPath = currentPath.split(/[\\/]/).slice(0, -1).join(currentPath.includes('\\') ? '\\' : '/');
|
||||||
|
setCurrentPath(parentPath);
|
||||||
|
loadAssets(parentPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canGoBack = currentPath && projectPath && currentPath !== projectPath;
|
||||||
|
|
||||||
const getFileIcon = (extension?: string) => {
|
const getFileIcon = (extension?: string) => {
|
||||||
switch (extension?.toLowerCase()) {
|
switch (extension?.toLowerCase()) {
|
||||||
case 'ts':
|
case 'ts':
|
||||||
@@ -156,7 +169,33 @@ export function AssetBrowser({ projectPath, locale }: AssetBrowserProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="asset-browser">
|
<div className="asset-browser">
|
||||||
<div className="asset-browser-header">
|
<div className="asset-browser-header">
|
||||||
<h3>{t.title}</h3>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||||
|
<h3 style={{ margin: 0 }}>{t.title}</h3>
|
||||||
|
{canGoBack && (
|
||||||
|
<button
|
||||||
|
onClick={handleBackToParent}
|
||||||
|
className="back-button"
|
||||||
|
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 className="asset-path">{currentPath}</div>
|
<div className="asset-path">{currentPath}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
165
packages/editor-app/src/components/DockContainer.tsx
Normal file
165
packages/editor-app/src/components/DockContainer.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { TabPanel, TabItem } from './TabPanel';
|
||||||
|
import { ResizablePanel } from './ResizablePanel';
|
||||||
|
import '../styles/DockContainer.css';
|
||||||
|
|
||||||
|
export type DockPosition = 'left' | 'right' | 'top' | 'bottom' | 'center';
|
||||||
|
|
||||||
|
export interface DockablePanel {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: ReactNode;
|
||||||
|
position: DockPosition;
|
||||||
|
closable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DockContainerProps {
|
||||||
|
panels: DockablePanel[];
|
||||||
|
onPanelClose?: (panelId: string) => void;
|
||||||
|
onPanelMove?: (panelId: string, newPosition: DockPosition) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DockContainer({ panels, onPanelClose }: DockContainerProps) {
|
||||||
|
const groupedPanels = panels.reduce((acc, panel) => {
|
||||||
|
if (!acc[panel.position]) {
|
||||||
|
acc[panel.position] = [];
|
||||||
|
}
|
||||||
|
acc[panel.position].push(panel);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<DockPosition, DockablePanel[]>);
|
||||||
|
|
||||||
|
const renderPanelGroup = (position: DockPosition) => {
|
||||||
|
const positionPanels = groupedPanels[position];
|
||||||
|
if (!positionPanels || positionPanels.length === 0) return null;
|
||||||
|
|
||||||
|
const tabs: TabItem[] = positionPanels.map(panel => ({
|
||||||
|
id: panel.id,
|
||||||
|
title: panel.title,
|
||||||
|
content: panel.content,
|
||||||
|
closable: panel.closable
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabPanel
|
||||||
|
tabs={tabs}
|
||||||
|
onTabClose={onPanelClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftPanel = groupedPanels['left'];
|
||||||
|
const rightPanel = groupedPanels['right'];
|
||||||
|
const topPanel = groupedPanels['top'];
|
||||||
|
const bottomPanel = groupedPanels['bottom'];
|
||||||
|
|
||||||
|
const hasLeft = leftPanel && leftPanel.length > 0;
|
||||||
|
const hasRight = rightPanel && rightPanel.length > 0;
|
||||||
|
const hasTop = topPanel && topPanel.length > 0;
|
||||||
|
const hasBottom = bottomPanel && bottomPanel.length > 0;
|
||||||
|
|
||||||
|
let content = (
|
||||||
|
<div className="dock-center">
|
||||||
|
{renderPanelGroup('center')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasTop || hasBottom) {
|
||||||
|
content = (
|
||||||
|
<ResizablePanel
|
||||||
|
direction="vertical"
|
||||||
|
defaultSize={200}
|
||||||
|
minSize={100}
|
||||||
|
maxSize={400}
|
||||||
|
leftOrTop={content}
|
||||||
|
rightOrBottom={
|
||||||
|
<div className="dock-bottom">
|
||||||
|
{renderPanelGroup('bottom')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTop) {
|
||||||
|
content = (
|
||||||
|
<ResizablePanel
|
||||||
|
direction="vertical"
|
||||||
|
defaultSize={200}
|
||||||
|
minSize={100}
|
||||||
|
maxSize={400}
|
||||||
|
leftOrTop={
|
||||||
|
<div className="dock-top">
|
||||||
|
{renderPanelGroup('top')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightOrBottom={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasLeft || hasRight) {
|
||||||
|
if (hasLeft && hasRight) {
|
||||||
|
content = (
|
||||||
|
<ResizablePanel
|
||||||
|
direction="horizontal"
|
||||||
|
defaultSize={250}
|
||||||
|
minSize={150}
|
||||||
|
maxSize={400}
|
||||||
|
leftOrTop={
|
||||||
|
<div className="dock-left">
|
||||||
|
{renderPanelGroup('left')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightOrBottom={
|
||||||
|
<ResizablePanel
|
||||||
|
direction="horizontal"
|
||||||
|
side="right"
|
||||||
|
defaultSize={280}
|
||||||
|
minSize={200}
|
||||||
|
maxSize={500}
|
||||||
|
leftOrTop={content}
|
||||||
|
rightOrBottom={
|
||||||
|
<div className="dock-right">
|
||||||
|
{renderPanelGroup('right')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (hasLeft) {
|
||||||
|
content = (
|
||||||
|
<ResizablePanel
|
||||||
|
direction="horizontal"
|
||||||
|
defaultSize={250}
|
||||||
|
minSize={150}
|
||||||
|
maxSize={400}
|
||||||
|
leftOrTop={
|
||||||
|
<div className="dock-left">
|
||||||
|
{renderPanelGroup('left')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
rightOrBottom={content}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<ResizablePanel
|
||||||
|
direction="horizontal"
|
||||||
|
side="right"
|
||||||
|
defaultSize={280}
|
||||||
|
minSize={200}
|
||||||
|
maxSize={500}
|
||||||
|
leftOrTop={content}
|
||||||
|
rightOrBottom={
|
||||||
|
<div className="dock-right">
|
||||||
|
{renderPanelGroup('right')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="dock-container">{content}</div>;
|
||||||
|
}
|
||||||
62
packages/editor-app/src/components/TabPanel.tsx
Normal file
62
packages/editor-app/src/components/TabPanel.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useState, ReactNode } from 'react';
|
||||||
|
import '../styles/TabPanel.css';
|
||||||
|
|
||||||
|
export interface TabItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: ReactNode;
|
||||||
|
closable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabPanelProps {
|
||||||
|
tabs: TabItem[];
|
||||||
|
activeTabId?: string;
|
||||||
|
onTabChange?: (tabId: string) => void;
|
||||||
|
onTabClose?: (tabId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabPanel({ tabs, activeTabId, onTabChange, onTabClose }: TabPanelProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState(activeTabId || tabs[0]?.id);
|
||||||
|
|
||||||
|
const handleTabClick = (tabId: string) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
onTabChange?.(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseTab = (e: React.MouseEvent, tabId: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTabClose?.(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentTab = tabs.find(tab => tab.id === activeTab);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tab-panel">
|
||||||
|
<div className="tab-header">
|
||||||
|
<div className="tab-list">
|
||||||
|
{tabs.map(tab => (
|
||||||
|
<div
|
||||||
|
key={tab.id}
|
||||||
|
className={`tab-item ${activeTab === tab.id ? 'active' : ''}`}
|
||||||
|
onClick={() => handleTabClick(tab.id)}
|
||||||
|
>
|
||||||
|
<span className="tab-title">{tab.title}</span>
|
||||||
|
{tab.closable && (
|
||||||
|
<button
|
||||||
|
className="tab-close"
|
||||||
|
onClick={(e) => handleCloseTab(e, tab.id)}
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="tab-content">
|
||||||
|
{currentTab?.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
packages/editor-app/src/styles/DockContainer.css
Normal file
33
packages/editor-app/src/styles/DockContainer.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.dock-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-left,
|
||||||
|
.dock-right,
|
||||||
|
.dock-top,
|
||||||
|
.dock-bottom,
|
||||||
|
.dock-center {
|
||||||
|
background: #1e1e1e;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-left {
|
||||||
|
border-right: 1px solid #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-right {
|
||||||
|
border-left: 1px solid #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-top {
|
||||||
|
border-bottom: 1px solid #3e3e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dock-bottom {
|
||||||
|
border-top: 1px solid #3e3e3e;
|
||||||
|
}
|
||||||
88
packages/editor-app/src/styles/TabPanel.css
Normal file
88
packages/editor-app/src/styles/TabPanel.css
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
.tab-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-header {
|
||||||
|
background: #252526;
|
||||||
|
border-bottom: 1px solid #3e3e3e;
|
||||||
|
min-height: 35px;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-list {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: transparent;
|
||||||
|
color: #969696;
|
||||||
|
border-right: 1px solid #3e3e3e;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
background: #2a2d2e;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #ffffff;
|
||||||
|
border-bottom: 2px solid #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #858585;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
background: #4e4e4e;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user