MAESTRO: Add colorblind-safe extension badge colors for file tabs

Use Wong's colorblind-safe palette for file extension badges when
colorBlindMode is enabled. Colors are distinguishable across
protanopia, deuteranopia, and tritanopia color vision deficiencies.

- Add COLORBLIND_EXTENSION_PALETTE to colorblindPalettes.ts
- Update getExtensionColor() in TabBar and TabSwitcherModal
- Pass colorBlindMode through App → MainPanel → TabBar
- Add 11 unit tests for colorblind extension badge colors
This commit is contained in:
Pedram Amini
2026-02-02 07:10:38 -06:00
parent 58c6331176
commit 4d66cae30c
8 changed files with 605 additions and 7 deletions

View File

@@ -4379,3 +4379,416 @@ describe('Extension badge styling across themes', () => {
expect(fileNameSpan).toHaveClass('max-w-[120px]');
});
});
describe('File tab extension badge colorblind mode', () => {
const mockOnTabSelect = vi.fn();
const mockOnTabClose = vi.fn();
const mockOnNewTab = vi.fn();
const mockOnFileTabSelect = vi.fn();
const mockOnFileTabClose = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
// 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 createTab = (overrides: Partial<AITab> = {}): AITab => ({
id: 'test-tab',
name: '',
agentSessionId: 'abc12345-def6-7890',
logs: [],
...overrides,
});
const createFileTab = (extension: string): FilePreviewTab => ({
id: `file-tab-${extension}`,
path: `/test/example${extension}`,
name: 'example',
extension: extension,
content: 'test content',
scrollTop: 0,
searchQuery: '',
editMode: false,
editContent: undefined,
createdAt: Date.now(),
lastModified: Date.now(),
});
it('renders colorblind-safe colors for TypeScript files in dark mode', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.ts');
expect(badge).toBeInTheDocument();
// Strong Blue (#0077BB) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(0, 119, 187, 0.35)' });
});
it('renders colorblind-safe colors for TypeScript files in light mode', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.tsx');
expect(badge).toBeInTheDocument();
// Strong Blue (#0077BB) lighter for light theme
expect(badge).toHaveStyle({ backgroundColor: 'rgba(0, 119, 187, 0.18)' });
});
it('renders colorblind-safe colors for Markdown files (teal)', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.md');
expect(badge).toBeInTheDocument();
// Teal (#009988) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(0, 153, 136, 0.35)' });
});
it('renders colorblind-safe colors for JSON/Config files (orange)', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.json');
expect(badge).toBeInTheDocument();
// Orange (#EE7733) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(238, 119, 51, 0.35)' });
});
it('renders colorblind-safe colors for CSS files (purple)', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.css');
expect(badge).toBeInTheDocument();
// Purple (#AA4499) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(170, 68, 153, 0.35)' });
});
it('renders colorblind-safe colors for HTML files (vermillion)', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.html');
expect(badge).toBeInTheDocument();
// Vermillion (#CC3311) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(204, 51, 17, 0.35)' });
});
it('renders colorblind-safe colors for Python files (cyan)', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.py');
expect(badge).toBeInTheDocument();
// Cyan (#33BBEE) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(51, 187, 238, 0.35)' });
});
it('renders colorblind-safe colors for Rust files (magenta)', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.rs');
expect(badge).toBeInTheDocument();
// Magenta (#EE3377) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(238, 51, 119, 0.35)' });
});
it('renders colorblind-safe colors for Go files (blue-green)', () => {
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
const fileTab = createFileTab('.go');
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.go');
expect(badge).toBeInTheDocument();
// Blue-Green (#44AA99) from Wong's colorblind-safe palette
expect(badge).toHaveStyle({ backgroundColor: 'rgba(68, 170, 153, 0.35)' });
});
it('renders colorblind-safe colors for Shell scripts (gray)', () => {
const aiTab = createTab({ id: 'ai-tab-1', name: 'AI Tab' });
const fileTab = createFileTab('.sh');
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.sh');
expect(badge).toBeInTheDocument();
// Gray for shell scripts (distinguishable by luminance)
expect(badge).toHaveStyle({ backgroundColor: 'rgba(150, 150, 150, 0.35)' });
});
it('falls back to theme colors for unknown extensions in colorblind mode', () => {
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}
colorBlindMode={true}
/>
);
const badge = screen.getByText('.xyz');
expect(badge).toBeInTheDocument();
// Falls back to theme border color for unknown extensions
expect(badge).toHaveStyle({ backgroundColor: darkTheme.colors.border });
});
});

View File

@@ -13068,6 +13068,9 @@ You are taking over this conversation. Based on the context above, provide a bri
// Unread filter
showUnreadOnly,
// Accessibility
colorBlindMode,
// Setters
setLogViewerSelectedLevels,
setGitDiffPreview,

View File

@@ -874,6 +874,8 @@ export interface AppUtilityModalsProps {
sessionName: string,
starred?: boolean
) => void;
/** Whether colorblind-friendly colors should be used for extension badges */
colorBlindMode?: boolean;
// FileSearchModal
fuzzyFileSearchOpen: boolean;
@@ -1051,6 +1053,7 @@ export function AppUtilityModals({
onTabSelect,
onFileTabSelect,
onNamedSessionSelect,
colorBlindMode,
// FileSearchModal
fuzzyFileSearchOpen,
filteredFileTree,
@@ -1255,6 +1258,7 @@ export function AppUtilityModals({
onFileTabSelect={onFileTabSelect}
onNamedSessionSelect={onNamedSessionSelect}
onClose={onCloseTabSwitcher}
colorBlindMode={colorBlindMode}
/>
)}
@@ -2551,6 +2555,7 @@ export function AppModals(props: AppModalsProps) {
onCloseTabSwitcher={onCloseTabSwitcher}
onTabSelect={onTabSelect}
onNamedSessionSelect={onNamedSessionSelect}
colorBlindMode={colorBlindMode}
fuzzyFileSearchOpen={fuzzyFileSearchOpen}
filteredFileTree={filteredFileTree}
onCloseFileSearch={onCloseFileSearch}

View File

@@ -197,6 +197,8 @@ interface MainPanelProps {
onToggleTabSaveToHistory?: () => void;
onToggleTabShowThinking?: () => void;
showUnreadOnly?: boolean;
/** Whether colorblind-friendly colors should be used for extension badges */
colorBlindMode?: boolean;
onToggleUnreadFilter?: () => void;
onOpenTabSearch?: () => void;
// Bulk tab close operations
@@ -471,6 +473,7 @@ export const MainPanel = React.memo(
onTabStar,
onTabMarkUnread,
showUnreadOnly,
colorBlindMode,
onToggleUnreadFilter,
onOpenTabSearch,
onCloseAllTabs,
@@ -1471,6 +1474,8 @@ export const MainPanel = React.memo(
activeFileTabId={activeFileTabId}
onFileTabSelect={onFileTabSelect}
onFileTabClose={onFileTabClose}
// Accessibility
colorBlindMode={colorBlindMode}
/>
)}

View File

@@ -24,6 +24,7 @@ import {
} from 'lucide-react';
import type { AITab, Theme, FilePreviewTab, UnifiedTab } from '../types';
import { hasDraft } from '../utils/tabHelpers';
import { getColorBlindExtensionColor } from '../constants/colorblindPalettes';
interface TabBarProps {
tabs: AITab[];
@@ -73,6 +74,10 @@ interface TabBarProps {
onFileTabSelect?: (tabId: string) => void;
/** Handler to close a file preview tab */
onFileTabClose?: (tabId: string) => void;
// === Accessibility ===
/** Whether colorblind-friendly colors should be used for extension badges */
colorBlindMode?: boolean;
}
interface TabProps {
@@ -946,17 +951,36 @@ interface FileTabProps {
totalTabs?: number;
/** Tab index in the full unified list (0-based) */
tabIndex?: number;
/** Whether colorblind-friendly colors should be used for extension badges */
colorBlindMode?: boolean;
}
/**
* 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.
* When colorBlindMode is enabled, uses Wong's colorblind-safe palette.
*/
function getExtensionColor(extension: string, theme: Theme): { bg: string; text: string } {
const ext = extension.toLowerCase();
function getExtensionColor(
extension: string,
theme: Theme,
colorBlindMode?: boolean
): { bg: string; text: string } {
const isLightTheme = theme.mode === 'light';
// Use colorblind-safe colors when enabled
if (colorBlindMode) {
const colorBlindColors = getColorBlindExtensionColor(extension, isLightTheme);
if (colorBlindColors) {
return colorBlindColors;
}
// Fall through to default for unknown extensions
return { bg: theme.colors.border, text: theme.colors.textDim };
}
// Standard color scheme
const ext = extension.toLowerCase();
// TypeScript/JavaScript - blue tones
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
return isLightTheme
@@ -1047,6 +1071,7 @@ const FileTab = memo(function FileTab({
onCloseTabsRight,
totalTabs,
tabIndex,
colorBlindMode,
}: FileTabProps) {
const [isHovered, setIsHovered] = useState(false);
const [overlayOpen, setOverlayOpen] = useState(false);
@@ -1243,8 +1268,8 @@ const FileTab = memo(function FileTab({
// Get extension badge colors
const extensionColors = useMemo(
() => getExtensionColor(tab.extension, theme),
[tab.extension, theme]
() => getExtensionColor(tab.extension, theme, colorBlindMode),
[tab.extension, theme, colorBlindMode]
);
// Hover background varies by theme mode for proper contrast
@@ -1587,6 +1612,8 @@ function TabBarInner({
onFileTabSelect,
onFileTabClose,
onUnifiedTabReorder,
// Accessibility
colorBlindMode,
}: TabBarProps) {
const [draggingTabId, setDraggingTabId] = useState<string | null>(null);
const [dragOverTabId, setDragOverTabId] = useState<string | null>(null);
@@ -2035,6 +2062,7 @@ function TabBarInner({
onCloseTabsRight={onCloseTabsRight ? handleTabCloseRight : undefined}
totalTabs={unifiedTabs!.length}
tabIndex={originalIndex}
colorBlindMode={colorBlindMode}
/>
</React.Fragment>
);

View File

@@ -9,6 +9,7 @@ import { getContextColor } from '../utils/theme';
import { formatShortcutKeys } from '../utils/shortcutFormatter';
import { formatTokensCompact, formatRelativeTime, formatCost } from '../utils/formatters';
import { calculateContextTokens } from '../utils/contextUsage';
import { getColorBlindExtensionColor } from '../constants/colorblindPalettes';
/** Named session from the store (not currently open) */
interface NamedSession {
@@ -47,6 +48,8 @@ interface TabSwitcherModalProps {
starred?: boolean
) => void;
onClose: () => void;
/** Whether colorblind-friendly colors should be used for extension badges */
colorBlindMode?: boolean;
}
// formatTokensCompact, formatRelativeTime, and formatCost imported from ../utils/formatters
@@ -108,9 +111,27 @@ function getUuidPill(agentSessionId: string | undefined | null): string | null {
/**
* Get color for file extension badge.
* Returns a muted color based on file type for visual differentiation.
* (Copied from TabBar.tsx for consistency)
* When colorBlindMode is enabled, uses Wong's colorblind-safe palette.
* (Synchronized with TabBar.tsx for consistency)
*/
function getExtensionColor(extension: string, theme: Theme): { bg: string; text: string } {
function getExtensionColor(
extension: string,
theme: Theme,
colorBlindMode?: boolean
): { bg: string; text: string } {
const isLightTheme = theme.mode === 'light';
// Use colorblind-safe colors when enabled
if (colorBlindMode) {
const colorBlindColors = getColorBlindExtensionColor(extension, isLightTheme);
if (colorBlindColors) {
return colorBlindColors;
}
// Fall through to default for unknown extensions
return { bg: theme.colors.border, text: theme.colors.textDim };
}
// Standard color scheme (dark theme only for backward compatibility)
const ext = extension.toLowerCase();
// TypeScript/JavaScript - blue tones
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
@@ -212,6 +233,7 @@ export function TabSwitcherModal({
onFileTabSelect,
onNamedSessionSelect,
onClose,
colorBlindMode,
}: TabSwitcherModalProps) {
const [search, setSearch] = useState('');
const [firstVisibleIndex, setFirstVisibleIndex] = useState(0);
@@ -776,7 +798,7 @@ export function TabSwitcherModal({
// File preview tab
const { tab } = item;
const isActive = tab.id === activeFileTabId;
const extColors = getExtensionColor(tab.extension, theme);
const extColors = getExtensionColor(tab.extension, theme, colorBlindMode);
const hasUnsavedEdits = !!tab.editContent;
return (

View File

@@ -114,3 +114,120 @@ export function getColorBlindPattern(index: number): ColorBlindPattern {
];
return patterns[index % patterns.length];
}
/**
* Colorblind-safe palette for file extension badges.
* Uses Wong's palette with appropriate contrast for badge backgrounds and text.
* Each extension category is mapped to a distinct, colorblind-safe color.
*
* Colors are chosen to be distinguishable in:
* - Protanopia (red-green, red-weak)
* - Deuteranopia (red-green, green-weak)
* - Tritanopia (blue-yellow)
*
* Each color has a light mode and dark mode variant for proper contrast.
*/
export const COLORBLIND_EXTENSION_PALETTE = {
// TypeScript/JavaScript - Strong Blue (#0077BB)
typescript: {
light: { bg: 'rgba(0, 119, 187, 0.18)', text: 'rgba(0, 90, 150, 0.95)' },
dark: { bg: 'rgba(0, 119, 187, 0.35)', text: 'rgba(102, 178, 230, 0.95)' },
},
// Markdown/Docs - Teal (#009988)
markdown: {
light: { bg: 'rgba(0, 153, 136, 0.18)', text: 'rgba(0, 115, 100, 0.95)' },
dark: { bg: 'rgba(0, 153, 136, 0.35)', text: 'rgba(77, 204, 189, 0.95)' },
},
// JSON/Config - Orange (#EE7733)
config: {
light: { bg: 'rgba(238, 119, 51, 0.18)', text: 'rgba(180, 85, 30, 0.95)' },
dark: { bg: 'rgba(238, 119, 51, 0.35)', text: 'rgba(255, 170, 120, 0.95)' },
},
// CSS/Styles - Purple (#AA4499)
styles: {
light: { bg: 'rgba(170, 68, 153, 0.18)', text: 'rgba(130, 50, 115, 0.95)' },
dark: { bg: 'rgba(170, 68, 153, 0.35)', text: 'rgba(210, 140, 195, 0.95)' },
},
// HTML/Templates - Vermillion (#CC3311)
html: {
light: { bg: 'rgba(204, 51, 17, 0.18)', text: 'rgba(160, 40, 15, 0.95)' },
dark: { bg: 'rgba(204, 51, 17, 0.35)', text: 'rgba(255, 130, 100, 0.95)' },
},
// Python - Cyan (#33BBEE)
python: {
light: { bg: 'rgba(51, 187, 238, 0.18)', text: 'rgba(30, 130, 175, 0.95)' },
dark: { bg: 'rgba(51, 187, 238, 0.35)', text: 'rgba(130, 210, 245, 0.95)' },
},
// Rust - Magenta (#EE3377)
rust: {
light: { bg: 'rgba(238, 51, 119, 0.18)', text: 'rgba(180, 35, 85, 0.95)' },
dark: { bg: 'rgba(238, 51, 119, 0.35)', text: 'rgba(255, 140, 175, 0.95)' },
},
// Go - Blue-Green (#44AA99)
go: {
light: { bg: 'rgba(68, 170, 153, 0.18)', text: 'rgba(45, 130, 115, 0.95)' },
dark: { bg: 'rgba(68, 170, 153, 0.35)', text: 'rgba(130, 210, 195, 0.95)' },
},
// Shell - Gray (#BBBBBB)
shell: {
light: { bg: 'rgba(120, 120, 120, 0.18)', text: 'rgba(80, 80, 80, 0.95)' },
dark: { bg: 'rgba(150, 150, 150, 0.35)', text: 'rgba(200, 200, 200, 0.95)' },
},
// Default/Unknown - uses theme colors (handled in getExtensionColor)
};
/**
* Get colorblind-safe color for file extension badges.
* Maps file extensions to colorblind-friendly colors from Wong's palette.
*
* @param extension - File extension including dot (e.g., '.ts', '.md')
* @param isLightTheme - Whether the current theme is light mode
* @returns Object with bg (background) and text color in rgba format
*/
export function getColorBlindExtensionColor(
extension: string,
isLightTheme: boolean
): { bg: string; text: string } | null {
const ext = extension.toLowerCase();
const mode = isLightTheme ? 'light' : 'dark';
// TypeScript/JavaScript
if (['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.typescript[mode];
}
// Markdown/Docs
if (['.md', '.mdx', '.txt', '.rst'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.markdown[mode];
}
// JSON/Config
if (['.json', '.yaml', '.yml', '.toml', '.ini', '.env'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.config[mode];
}
// CSS/Styles
if (['.css', '.scss', '.sass', '.less', '.styl'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.styles[mode];
}
// HTML/Templates
if (['.html', '.htm', '.xml', '.svg'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.html[mode];
}
// Python
if (['.py', '.pyw', '.pyi'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.python[mode];
}
// Rust
if (['.rs'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.rust[mode];
}
// Go
if (['.go'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.go[mode];
}
// Shell scripts
if (['.sh', '.bash', '.zsh', '.fish'].includes(ext)) {
return COLORBLIND_EXTENSION_PALETTE.shell[mode];
}
// Return null for unknown extensions (caller should use theme defaults)
return null;
}

View File

@@ -133,6 +133,9 @@ export interface UseMainPanelPropsDeps {
// Unread filter
showUnreadOnly: boolean;
// Accessibility
colorBlindMode: boolean;
// Setters (these are stable callbacks - should be memoized at definition site)
setLogViewerSelectedLevels: (levels: string[]) => void;
setGitDiffPreview: (preview: string | null) => void;
@@ -403,6 +406,7 @@ export function useMainPanelProps(deps: UseMainPanelPropsDeps) {
onTabMarkUnread: deps.handleTabMarkUnread,
onToggleTabReadOnlyMode: deps.handleToggleTabReadOnlyMode,
showUnreadOnly: deps.showUnreadOnly,
colorBlindMode: deps.colorBlindMode,
onToggleUnreadFilter: deps.toggleUnreadFilter,
onOpenTabSearch: deps.handleOpenTabSearch,
onCloseAllTabs: deps.handleCloseAllTabs,
@@ -579,6 +583,7 @@ export function useMainPanelProps(deps: UseMainPanelPropsDeps) {
deps.ghCliAvailable,
deps.hasGist,
deps.showUnreadOnly,
deps.colorBlindMode,
// Stable callbacks (shouldn't cause re-renders, but included for completeness)
deps.setLogViewerSelectedLevels,
deps.setGitDiffPreview,