mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
MAESTRO: Add visual polish for file tab styling and theme-aware extension badges
- Fix max-width truncation inconsistency: file tabs now use max-w-[120px] to match AI tabs - Add theme-aware hover background: dark mode (rgba(255,255,255,0.08)), light mode (rgba(0,0,0,0.06)) - Enhance getExtensionColor() with theme-aware colors for both light and dark modes: - TypeScript/JavaScript, Markdown, JSON, CSS, HTML with contrast-appropriate colors - Add support for Python (teal), Rust (red), Go (cyan), Shell (gray) - Unknown extensions fall back to theme border/textDim colors - Add 10 unit tests verifying extension badge colors across themes
This commit is contained in:
@@ -3994,3 +3994,388 @@ describe('File tab content and SSH support', () => {
|
||||
expect(fileTab.editMode).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Extension badge styling tests for visual polish across themes
|
||||
describe('Extension badge styling across themes', () => {
|
||||
const mockOnTabSelect = vi.fn();
|
||||
const mockOnTabClose = vi.fn();
|
||||
const mockOnNewTab = vi.fn();
|
||||
const mockOnFileTabSelect = vi.fn();
|
||||
const mockOnFileTabClose = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.clearAllMocks();
|
||||
Element.prototype.scrollTo = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// Light theme for testing contrast
|
||||
const lightTheme: Theme = {
|
||||
id: 'github-light',
|
||||
name: 'GitHub Light',
|
||||
mode: 'light',
|
||||
colors: {
|
||||
bgMain: '#ffffff',
|
||||
bgSidebar: '#f6f8fa',
|
||||
bgActivity: '#eff2f5',
|
||||
textMain: '#24292f',
|
||||
textDim: '#57606a',
|
||||
accent: '#0969da',
|
||||
border: '#d0d7de',
|
||||
error: '#cf222e',
|
||||
success: '#1a7f37',
|
||||
warning: '#9a6700',
|
||||
},
|
||||
};
|
||||
|
||||
// Dark theme for comparison
|
||||
const darkTheme: Theme = {
|
||||
id: 'dracula',
|
||||
name: 'Dracula',
|
||||
mode: 'dark',
|
||||
colors: {
|
||||
bgMain: '#282a36',
|
||||
bgSidebar: '#21222c',
|
||||
bgActivity: '#343746',
|
||||
textMain: '#f8f8f2',
|
||||
textDim: '#6272a4',
|
||||
accent: '#bd93f9',
|
||||
border: '#44475a',
|
||||
error: '#ff5555',
|
||||
success: '#50fa7b',
|
||||
warning: '#ffb86c',
|
||||
},
|
||||
};
|
||||
|
||||
const createFileTab = (extension: string): FilePreviewTab => ({
|
||||
id: `file-tab-${extension}`,
|
||||
path: `/test/file${extension}`,
|
||||
name: 'file',
|
||||
extension: extension,
|
||||
content: 'test content',
|
||||
scrollTop: 0,
|
||||
searchQuery: '',
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
});
|
||||
|
||||
it('renders extension badges for TypeScript files with appropriate styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.ts');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
// Extension badge should be rendered
|
||||
const badge = screen.getByText('.ts');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Badge should have blue-ish background for TypeScript
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(59, 130, 246, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for TypeScript files with light theme appropriate styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.tsx');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={lightTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
// Extension badge should be rendered with light theme colors
|
||||
const badge = screen.getByText('.tsx');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Badge should have darker blue for better contrast on light backgrounds
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(37, 99, 235, 0.15)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for Markdown files with dark theme styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.md');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.md');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Green tones for Markdown/Docs
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(34, 197, 94, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for JSON files with dark theme styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.json');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.json');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Yellow tones for JSON/Config
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(234, 179, 8, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for CSS files with dark theme styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.css');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.css');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Purple tones for CSS/Styles
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(168, 85, 247, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for HTML files with dark theme styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.html');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.html');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Orange tones for HTML/Templates
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(249, 115, 22, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for Python files with dark theme styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.py');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.py');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Teal/cyan tones for Python
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(20, 184, 166, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for Rust files with dark theme styling', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.rs');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.rs');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Rust/red-orange tones for Rust
|
||||
expect(badge).toHaveStyle({ backgroundColor: 'rgba(239, 68, 68, 0.3)' });
|
||||
});
|
||||
|
||||
it('renders extension badges for unknown files using theme border color', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab = createFileTab('.xyz');
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1"
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
const badge = screen.getByText('.xyz');
|
||||
expect(badge).toBeInTheDocument();
|
||||
// Uses theme border color for unknown extensions
|
||||
expect(badge).toHaveStyle({ backgroundColor: darkTheme.colors.border });
|
||||
});
|
||||
|
||||
it('renders consistent tab name truncation for file tabs (max-w-[120px])', () => {
|
||||
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
|
||||
const fileTab: FilePreviewTab = {
|
||||
id: 'file-tab-1',
|
||||
path: '/test/very-long-filename-that-should-be-truncated.ts',
|
||||
name: 'very-long-filename-that-should-be-truncated',
|
||||
extension: '.ts',
|
||||
content: 'test',
|
||||
scrollTop: 0,
|
||||
searchQuery: '',
|
||||
editMode: false,
|
||||
editContent: undefined,
|
||||
createdAt: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
};
|
||||
|
||||
const unifiedTabs = [
|
||||
{ type: 'ai' as const, id: 'ai-tab-1', data: aiTab },
|
||||
{ type: 'file' as const, id: fileTab.id, data: fileTab },
|
||||
];
|
||||
|
||||
render(
|
||||
<TabBar
|
||||
tabs={[aiTab]}
|
||||
activeTabId="ai-tab-1" // AI tab active, file tab inactive
|
||||
theme={darkTheme}
|
||||
onTabSelect={mockOnTabSelect}
|
||||
onTabClose={mockOnTabClose}
|
||||
onNewTab={mockOnNewTab}
|
||||
unifiedTabs={unifiedTabs}
|
||||
activeFileTabId={null}
|
||||
onFileTabSelect={mockOnFileTabSelect}
|
||||
onFileTabClose={mockOnFileTabClose}
|
||||
/>
|
||||
);
|
||||
|
||||
// File tab name span should have truncation class
|
||||
const fileNameSpan = screen.getByText('very-long-filename-that-should-be-truncated');
|
||||
expect(fileNameSpan).toHaveClass('truncate');
|
||||
expect(fileNameSpan).toHaveClass('max-w-[120px]');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -476,6 +476,9 @@ const Tab = memo(function Tab({
|
||||
// Memoize display name to avoid recalculation on every render
|
||||
const displayName = useMemo(() => getTabDisplayName(tab), [tab.name, tab.agentSessionId]);
|
||||
|
||||
// Hover background varies by theme mode for proper contrast
|
||||
const hoverBgColor = theme.mode === 'light' ? 'rgba(0, 0, 0, 0.06)' : 'rgba(255, 255, 255, 0.08)';
|
||||
|
||||
// Memoize tab styles to avoid creating new object references on every render
|
||||
const tabStyle = useMemo(
|
||||
() =>
|
||||
@@ -485,11 +488,7 @@ const Tab = memo(function Tab({
|
||||
borderTopRightRadius: '6px',
|
||||
// Active tab: bright background matching content area
|
||||
// Inactive tabs: transparent with subtle hover
|
||||
backgroundColor: isActive
|
||||
? theme.colors.bgMain
|
||||
: isHovered
|
||||
? 'rgba(255, 255, 255, 0.08)'
|
||||
: 'transparent',
|
||||
backgroundColor: isActive ? theme.colors.bgMain : isHovered ? hoverBgColor : 'transparent',
|
||||
// Active tab has visible borders, inactive tabs have no borders (cleaner look)
|
||||
borderTop: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
|
||||
borderLeft: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
|
||||
@@ -502,7 +501,7 @@ const Tab = memo(function Tab({
|
||||
zIndex: isActive ? 1 : 0,
|
||||
'--tw-ring-color': isDragOver ? theme.colors.accent : 'transparent',
|
||||
}) as React.CSSProperties,
|
||||
[isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent]
|
||||
[isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent, hoverBgColor]
|
||||
);
|
||||
|
||||
// Browser-style tab: all tabs have borders, active tab "connects" to content
|
||||
@@ -952,28 +951,65 @@ interface FileTabProps {
|
||||
/**
|
||||
* Get color for file extension badge.
|
||||
* Returns a muted color based on file type for visual differentiation.
|
||||
* Colors are adapted for both light and dark themes for good contrast.
|
||||
*/
|
||||
function getExtensionColor(extension: string, theme: Theme): { bg: string; text: string } {
|
||||
const ext = extension.toLowerCase();
|
||||
const isLightTheme = theme.mode === 'light';
|
||||
|
||||
// TypeScript/JavaScript - blue tones
|
||||
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
|
||||
return { bg: 'rgba(59, 130, 246, 0.3)', text: 'rgba(147, 197, 253, 0.9)' };
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(37, 99, 235, 0.15)', text: 'rgba(29, 78, 216, 0.9)' }
|
||||
: { bg: 'rgba(59, 130, 246, 0.3)', text: 'rgba(147, 197, 253, 0.9)' };
|
||||
}
|
||||
// Markdown/Docs - green tones
|
||||
if (['.md', '.mdx', '.txt', '.rst'].includes(ext)) {
|
||||
return { bg: 'rgba(34, 197, 94, 0.3)', text: 'rgba(134, 239, 172, 0.9)' };
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(22, 163, 74, 0.15)', text: 'rgba(21, 128, 61, 0.9)' }
|
||||
: { bg: 'rgba(34, 197, 94, 0.3)', text: 'rgba(134, 239, 172, 0.9)' };
|
||||
}
|
||||
// JSON/Config - yellow tones
|
||||
// JSON/Config - yellow/amber tones
|
||||
if (['.json', '.yaml', '.yml', '.toml', '.ini', '.env'].includes(ext)) {
|
||||
return { bg: 'rgba(234, 179, 8, 0.3)', text: 'rgba(253, 224, 71, 0.9)' };
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(217, 119, 6, 0.15)', text: 'rgba(180, 83, 9, 0.9)' }
|
||||
: { bg: 'rgba(234, 179, 8, 0.3)', text: 'rgba(253, 224, 71, 0.9)' };
|
||||
}
|
||||
// CSS/Styles - purple tones
|
||||
if (['.css', '.scss', '.sass', '.less', '.styl'].includes(ext)) {
|
||||
return { bg: 'rgba(168, 85, 247, 0.3)', text: 'rgba(216, 180, 254, 0.9)' };
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(147, 51, 234, 0.15)', text: 'rgba(126, 34, 206, 0.9)' }
|
||||
: { bg: 'rgba(168, 85, 247, 0.3)', text: 'rgba(216, 180, 254, 0.9)' };
|
||||
}
|
||||
// HTML/Templates - orange tones
|
||||
if (['.html', '.htm', '.xml', '.svg'].includes(ext)) {
|
||||
return { bg: 'rgba(249, 115, 22, 0.3)', text: 'rgba(253, 186, 116, 0.9)' };
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(234, 88, 12, 0.15)', text: 'rgba(194, 65, 12, 0.9)' }
|
||||
: { bg: 'rgba(249, 115, 22, 0.3)', text: 'rgba(253, 186, 116, 0.9)' };
|
||||
}
|
||||
// Python - teal/cyan tones
|
||||
if (['.py', '.pyw', '.pyi'].includes(ext)) {
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(13, 148, 136, 0.15)', text: 'rgba(15, 118, 110, 0.9)' }
|
||||
: { bg: 'rgba(20, 184, 166, 0.3)', text: 'rgba(94, 234, 212, 0.9)' };
|
||||
}
|
||||
// Rust - rust/orange-red tones
|
||||
if (['.rs'].includes(ext)) {
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(185, 28, 28, 0.15)', text: 'rgba(153, 27, 27, 0.9)' }
|
||||
: { bg: 'rgba(239, 68, 68, 0.3)', text: 'rgba(252, 165, 165, 0.9)' };
|
||||
}
|
||||
// Go - cyan tones
|
||||
if (['.go'].includes(ext)) {
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(8, 145, 178, 0.15)', text: 'rgba(14, 116, 144, 0.9)' }
|
||||
: { bg: 'rgba(6, 182, 212, 0.3)', text: 'rgba(103, 232, 249, 0.9)' };
|
||||
}
|
||||
// Shell scripts - gray/slate tones
|
||||
if (['.sh', '.bash', '.zsh', '.fish'].includes(ext)) {
|
||||
return isLightTheme
|
||||
? { bg: 'rgba(71, 85, 105, 0.15)', text: 'rgba(51, 65, 85, 0.9)' }
|
||||
: { bg: 'rgba(100, 116, 139, 0.3)', text: 'rgba(203, 213, 225, 0.9)' };
|
||||
}
|
||||
// Default - use theme's dim colors
|
||||
return { bg: theme.colors.border, text: theme.colors.textDim };
|
||||
@@ -1211,6 +1247,9 @@ const FileTab = memo(function FileTab({
|
||||
[tab.extension, theme]
|
||||
);
|
||||
|
||||
// Hover background varies by theme mode for proper contrast
|
||||
const hoverBgColor = theme.mode === 'light' ? 'rgba(0, 0, 0, 0.06)' : 'rgba(255, 255, 255, 0.08)';
|
||||
|
||||
// Memoize tab styles to avoid creating new object references on every render
|
||||
const tabStyle = useMemo(
|
||||
() =>
|
||||
@@ -1220,11 +1259,7 @@ const FileTab = memo(function FileTab({
|
||||
borderTopRightRadius: '6px',
|
||||
// Active tab: bright background matching content area
|
||||
// Inactive tabs: transparent with subtle hover
|
||||
backgroundColor: isActive
|
||||
? theme.colors.bgMain
|
||||
: isHovered
|
||||
? 'rgba(255, 255, 255, 0.08)'
|
||||
: 'transparent',
|
||||
backgroundColor: isActive ? theme.colors.bgMain : isHovered ? hoverBgColor : 'transparent',
|
||||
// Active tab has visible borders, inactive tabs have no borders (cleaner look)
|
||||
borderTop: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
|
||||
borderLeft: isActive ? `1px solid ${theme.colors.border}` : '1px solid transparent',
|
||||
@@ -1237,7 +1272,7 @@ const FileTab = memo(function FileTab({
|
||||
zIndex: isActive ? 1 : 0,
|
||||
'--tw-ring-color': isDragOver ? theme.colors.accent : 'transparent',
|
||||
}) as React.CSSProperties,
|
||||
[isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent]
|
||||
[isActive, isHovered, isDragOver, theme.colors.bgMain, theme.colors.border, theme.colors.accent, hoverBgColor]
|
||||
);
|
||||
|
||||
// Check if tab has unsaved edits
|
||||
@@ -1273,7 +1308,7 @@ const FileTab = memo(function FileTab({
|
||||
|
||||
{/* Tab name - filename without extension */}
|
||||
<span
|
||||
className={`text-xs font-medium ${isActive ? 'whitespace-nowrap' : 'truncate max-w-[100px]'}`}
|
||||
className={`text-xs font-medium ${isActive ? 'whitespace-nowrap' : 'truncate max-w-[120px]'}`}
|
||||
style={{ color: isActive ? theme.colors.textMain : theme.colors.textDim }}
|
||||
>
|
||||
{tab.name}
|
||||
|
||||
Reference in New Issue
Block a user