Skip to content

Commit fdd6ace

Browse files
committed
feat(ui): add dark mode icon inversion for improved visibility
Introduce `iconNeedsInvert` property to `ProviderFilterItem` and icon info. Implement `invert-on-dark` CSS class to conditionally invert image colors. Apply inversion logic to provider icons, like Copilot, for better visibility in both light and dark themes. This enhances visual consistency and usability.
1 parent cdfb2f8 commit fdd6ace

4 files changed

Lines changed: 141 additions & 122 deletions

File tree

src/components/quota/ProviderFilter.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface ProviderFilterItem {
77
label: string;
88
count: number;
99
icon?: string;
10+
iconNeedsInvert?: boolean;
1011
}
1112

1213
interface ProviderFilterProps {
@@ -33,7 +34,11 @@ export function ProviderFilter({ items, activeId, onSelect }: ProviderFilterProp
3334
)}
3435
>
3536
{item.icon && (
36-
<img src={item.icon} alt={item.label} className="h-4 w-4 object-contain" />
37+
<img
38+
src={item.icon}
39+
alt={item.label}
40+
className={cn("h-4 w-4 object-contain", item.iconNeedsInvert && "invert-on-dark")}
41+
/>
3742
)}
3843

3944
<span className="text-sm font-medium">{item.label}</span>

src/index.css

Lines changed: 102 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -104,58 +104,58 @@ html.dark {
104104
}
105105

106106
:root {
107-
--background: oklch(1 0 0);
108-
--foreground: oklch(0.1450 0 0);
109-
--card: oklch(1 0 0);
110-
--card-foreground: oklch(0.1450 0 0);
111-
--popover: oklch(1 0 0);
112-
--popover-foreground: oklch(0.1450 0 0);
113-
--primary: oklch(0.2050 0 0);
114-
--primary-foreground: oklch(0.9850 0 0);
115-
--secondary: oklch(0.9700 0 0);
116-
--secondary-foreground: oklch(0.2050 0 0);
117-
--muted: oklch(0.9700 0 0);
118-
--muted-foreground: oklch(0.5560 0 0);
119-
--accent: oklch(0.9700 0 0);
120-
--accent-foreground: oklch(0.2050 0 0);
121-
--destructive: oklch(0.5770 0.2450 27.3250);
122-
--destructive-foreground: oklch(1 0 0);
123-
--border: oklch(0.9220 0 0);
124-
--input: oklch(0.9220 0 0);
125-
--ring: oklch(0.7080 0 0);
126-
--chart-1: oklch(0.8100 0.1000 252);
127-
--chart-2: oklch(0.6200 0.1900 260);
128-
--chart-3: oklch(0.5500 0.2200 263);
129-
--chart-4: oklch(0.4900 0.2200 264);
130-
--chart-5: oklch(0.4200 0.1800 266);
131-
--sidebar: oklch(0.9850 0 0);
132-
--sidebar-foreground: oklch(0.1450 0 0);
133-
--sidebar-primary: oklch(0.2050 0 0);
134-
--sidebar-primary-foreground: oklch(0.9850 0 0);
135-
--sidebar-accent: oklch(0.9700 0 0);
136-
--sidebar-accent-foreground: oklch(0.2050 0 0);
137-
--sidebar-border: oklch(0.9220 0 0);
138-
--sidebar-ring: oklch(0.7080 0 0);
139-
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
140-
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
141-
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
142-
--radius: 0.625rem;
143-
--shadow-x: 0;
144-
--shadow-y: 1px;
145-
--shadow-blur: 3px;
146-
--shadow-spread: 0px;
147-
--shadow-opacity: 0.1;
148-
--shadow-color: oklch(0 0 0);
149-
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
150-
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
151-
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
152-
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
153-
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
154-
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
155-
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
156-
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
157-
--tracking-normal: 0em;
158-
--spacing: 0.25rem;
107+
--background: oklch(1 0 0);
108+
--foreground: oklch(0.1450 0 0);
109+
--card: oklch(1 0 0);
110+
--card-foreground: oklch(0.1450 0 0);
111+
--popover: oklch(1 0 0);
112+
--popover-foreground: oklch(0.1450 0 0);
113+
--primary: oklch(0.2050 0 0);
114+
--primary-foreground: oklch(0.9850 0 0);
115+
--secondary: oklch(0.9700 0 0);
116+
--secondary-foreground: oklch(0.2050 0 0);
117+
--muted: oklch(0.9700 0 0);
118+
--muted-foreground: oklch(0.5560 0 0);
119+
--accent: oklch(0.9700 0 0);
120+
--accent-foreground: oklch(0.2050 0 0);
121+
--destructive: oklch(0.5770 0.2450 27.3250);
122+
--destructive-foreground: oklch(1 0 0);
123+
--border: oklch(0.9220 0 0);
124+
--input: oklch(0.9220 0 0);
125+
--ring: oklch(0.7080 0 0);
126+
--chart-1: oklch(0.8100 0.1000 252);
127+
--chart-2: oklch(0.6200 0.1900 260);
128+
--chart-3: oklch(0.5500 0.2200 263);
129+
--chart-4: oklch(0.4900 0.2200 264);
130+
--chart-5: oklch(0.4200 0.1800 266);
131+
--sidebar: oklch(0.9850 0 0);
132+
--sidebar-foreground: oklch(0.1450 0 0);
133+
--sidebar-primary: oklch(0.2050 0 0);
134+
--sidebar-primary-foreground: oklch(0.9850 0 0);
135+
--sidebar-accent: oklch(0.9700 0 0);
136+
--sidebar-accent-foreground: oklch(0.2050 0 0);
137+
--sidebar-border: oklch(0.9220 0 0);
138+
--sidebar-ring: oklch(0.7080 0 0);
139+
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
140+
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
141+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
142+
--radius: 0.625rem;
143+
--shadow-x: 0;
144+
--shadow-y: 1px;
145+
--shadow-blur: 3px;
146+
--shadow-spread: 0px;
147+
--shadow-opacity: 0.1;
148+
--shadow-color: oklch(0 0 0);
149+
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
150+
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
151+
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
152+
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
153+
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
154+
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
155+
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
156+
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
157+
--tracking-normal: 0em;
158+
--spacing: 0.25rem;
159159
}
160160

161161

@@ -170,46 +170,46 @@ html.dark {
170170
--primary-foreground: oklch(0.2050 0 0);
171171
--secondary: oklch(0.2690 0 0);
172172
--secondary-foreground: oklch(0.9850 0 0);
173-
--muted: oklch(0.2690 0 0);
174-
--muted-foreground: oklch(0.7080 0 0);
175-
--accent: oklch(0.3710 0 0);
176-
--accent-foreground: oklch(0.9850 0 0);
177-
--destructive: oklch(0.7040 0.1910 22.2160);
178-
--destructive-foreground: oklch(0.9850 0 0);
179-
--border: oklch(0.2750 0 0);
180-
--input: oklch(0.3250 0 0);
181-
--ring: oklch(0.5560 0 0);
182-
--chart-1: oklch(0.8100 0.1000 252);
183-
--chart-2: oklch(0.6200 0.1900 260);
184-
--chart-3: oklch(0.5500 0.2200 263);
185-
--chart-4: oklch(0.4900 0.2200 264);
186-
--chart-5: oklch(0.4200 0.1800 266);
187-
--sidebar: oklch(0.2050 0 0);
188-
--sidebar-foreground: oklch(0.9850 0 0);
189-
--sidebar-primary: oklch(0.4880 0.2430 264.3760);
190-
--sidebar-primary-foreground: oklch(0.9850 0 0);
191-
--sidebar-accent: oklch(0.2690 0 0);
192-
--sidebar-accent-foreground: oklch(0.9850 0 0);
193-
--sidebar-border: oklch(0.2750 0 0);
194-
--sidebar-ring: oklch(0.4390 0 0);
195-
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
196-
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
197-
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
198-
--radius: 0.625rem;
199-
--shadow-x: 0;
200-
--shadow-y: 1px;
201-
--shadow-blur: 3px;
202-
--shadow-spread: 0px;
203-
--shadow-opacity: 0.1;
204-
--shadow-color: oklch(0 0 0);
205-
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
206-
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
207-
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
208-
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
209-
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
210-
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
211-
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
212-
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
173+
--muted: oklch(0.2690 0 0);
174+
--muted-foreground: oklch(0.7080 0 0);
175+
--accent: oklch(0.3710 0 0);
176+
--accent-foreground: oklch(0.9850 0 0);
177+
--destructive: oklch(0.7040 0.1910 22.2160);
178+
--destructive-foreground: oklch(0.9850 0 0);
179+
--border: oklch(0.2750 0 0);
180+
--input: oklch(0.3250 0 0);
181+
--ring: oklch(0.5560 0 0);
182+
--chart-1: oklch(0.8100 0.1000 252);
183+
--chart-2: oklch(0.6200 0.1900 260);
184+
--chart-3: oklch(0.5500 0.2200 263);
185+
--chart-4: oklch(0.4900 0.2200 264);
186+
--chart-5: oklch(0.4200 0.1800 266);
187+
--sidebar: oklch(0.2050 0 0);
188+
--sidebar-foreground: oklch(0.9850 0 0);
189+
--sidebar-primary: oklch(0.4880 0.2430 264.3760);
190+
--sidebar-primary-foreground: oklch(0.9850 0 0);
191+
--sidebar-accent: oklch(0.2690 0 0);
192+
--sidebar-accent-foreground: oklch(0.9850 0 0);
193+
--sidebar-border: oklch(0.2750 0 0);
194+
--sidebar-ring: oklch(0.4390 0 0);
195+
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
196+
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
197+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
198+
--radius: 0.625rem;
199+
--shadow-x: 0;
200+
--shadow-y: 1px;
201+
--shadow-blur: 3px;
202+
--shadow-spread: 0px;
203+
--shadow-opacity: 0.1;
204+
--shadow-color: oklch(0 0 0);
205+
--shadow-2xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
206+
--shadow-xs: 0 1px 3px 0px hsl(0 0% 0% / 0.05);
207+
--shadow-sm: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
208+
--shadow: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 1px 2px -1px hsl(0 0% 0% / 0.10);
209+
--shadow-md: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 2px 4px -1px hsl(0 0% 0% / 0.10);
210+
--shadow-lg: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 4px 6px -1px hsl(0 0% 0% / 0.10);
211+
--shadow-xl: 0 1px 3px 0px hsl(0 0% 0% / 0.10), 0 8px 10px -1px hsl(0 0% 0% / 0.10);
212+
--shadow-2xl: 0 1px 3px 0px hsl(0 0% 0% / 0.25);
213213
}
214214

215215
.no-scrollbar::-webkit-scrollbar {
@@ -233,3 +233,13 @@ html.dark {
233233
letter-spacing: var(--tracking-normal);
234234
}
235235
}
236+
237+
/* Utility class for logos that need to invert in dark mode */
238+
/* Dark logo becomes white in dark mode */
239+
.invert-on-dark {
240+
filter: none;
241+
}
242+
243+
.dark .invert-on-dark {
244+
filter: invert(1);
245+
}

src/pages/ProvidersPage.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,16 @@ function formatName(name: string | undefined | null): string {
7676
return name.replace(/_gmail_com/g, '').replace(/\.json$/g, '');
7777
}
7878

79-
// Helper to get provider icon path
80-
function getProviderIconPath(providerId: string): string {
79+
// Helper to get provider icon path and whether it needs dark mode inversion
80+
function getProviderIconInfo(providerId: string): { path: string; needsInvert: boolean } {
8181
const id = providerId.toLowerCase();
82-
if (id.includes('antigravity')) return '/antigravity/antigravity.png';
83-
if (id.includes('claude') || id.includes('anthropic')) return '/claude/claude.png';
84-
if (id.includes('gemini')) return '/gemini/gemini.png';
85-
if (id.includes('codex') || id.includes('openai')) return '/openai/openai.png';
86-
if (id.includes('kiro')) return '/kiro/kiro.png';
87-
if (id.includes('copilot') || id.includes('github')) return '/copilot/copilot.png';
88-
return '';
82+
if (id.includes('antigravity')) return { path: '/antigravity/antigravity.png', needsInvert: false };
83+
if (id.includes('claude') || id.includes('anthropic')) return { path: '/claude/claude.png', needsInvert: false };
84+
if (id.includes('gemini')) return { path: '/gemini/gemini.png', needsInvert: false };
85+
if (id.includes('codex') || id.includes('openai')) return { path: '/openai/openai.png', needsInvert: false };
86+
if (id.includes('kiro')) return { path: '/kiro/kiro.png', needsInvert: false };
87+
if (id.includes('copilot') || id.includes('github')) return { path: '/copilot/copilot.png', needsInvert: true };
88+
return { path: '', needsInvert: false };
8989
}
9090

9191

@@ -428,7 +428,7 @@ export function ProvidersPage() {
428428

429429
<div className="space-y-1">
430430
{files.map((file) => {
431-
const iconPath = getProviderIconPath(file.provider || '');
431+
const iconInfo = getProviderIconInfo(file.provider || '');
432432
const p = (file.provider || '').toLowerCase();
433433

434434
let rawName: string;
@@ -469,9 +469,9 @@ export function ProvidersPage() {
469469
{/* Provider Icon */}
470470
<div className="flex h-8 w-8 items-center justify-center rounded-sm overflow-hidden bg-transparent">
471471
<img
472-
src={iconPath}
472+
src={iconInfo.path}
473473
alt={file.provider}
474-
className="h-full w-full object-contain"
474+
className={`h-full w-full object-contain ${iconInfo.needsInvert ? 'invert-on-dark' : ''}`}
475475
onError={(e) => {
476476
(e.target as HTMLImageElement).style.display = 'none';
477477
((e.target as HTMLImageElement).nextSibling as HTMLElement).style.display = 'block';
@@ -525,7 +525,7 @@ export function ProvidersPage() {
525525
const isWaiting = state.status === 'waiting' || state.status === 'polling';
526526
const isSuccess = state.status === 'success';
527527

528-
const iconPath = getProviderIconPath(provider.id);
528+
const iconInfo = getProviderIconInfo(provider.id);
529529

530530
const isSelected = selectedProvider === provider.id;
531531

@@ -649,9 +649,9 @@ export function ProvidersPage() {
649649
{/* Icon */}
650650
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-secondary/50 p-1 overflow-hidden">
651651
<img
652-
src={iconPath}
652+
src={iconInfo.path}
653653
alt={provider.name}
654-
className="h-full w-full object-contain"
654+
className={`h-full w-full object-contain ${iconInfo.needsInvert ? 'invert-on-dark' : ''}`}
655655
onError={(e) => {
656656
(e.target as HTMLImageElement).style.display = 'none';
657657
}}

src/pages/QuotaPage.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -289,24 +289,28 @@ export function QuotaPage() {
289289

290290
// Compute filter items for header
291291
const filterItems: ProviderFilterItem[] = useMemo(() => {
292-
// Map internal keys to icon paths (assume standard location public/provider/provider.png)
293-
const getIcon = (key: string) => {
294-
if (key === 'antigravity') return '/antigravity/antigravity.png';
295-
if (key === 'codex') return '/openai/openai.png'; // Assuming Codex uses OpenAI icon
296-
if (key === 'gemini-cli') return '/gemini/gemini.png';
297-
if (key === 'kiro') return '/kiro/kiro.png';
298-
if (key === 'copilot') return '/copilot/copilot.png';
299-
return undefined;
292+
// Map internal keys to icon paths and dark mode inversion needs
293+
const getIconInfo = (key: string): { path?: string; needsInvert: boolean } => {
294+
if (key === 'antigravity') return { path: '/antigravity/antigravity.png', needsInvert: false };
295+
if (key === 'codex') return { path: '/openai/openai.png', needsInvert: false };
296+
if (key === 'gemini-cli') return { path: '/gemini/gemini.png', needsInvert: false };
297+
if (key === 'kiro') return { path: '/kiro/kiro.png', needsInvert: false };
298+
if (key === 'copilot') return { path: '/copilot/copilot.png', needsInvert: true };
299+
return { needsInvert: false };
300300
};
301301

302302
return sections
303303
.filter(s => s.files.length > 0)
304-
.map(s => ({
305-
id: s.provider,
306-
label: s.displayName,
307-
count: s.files.length,
308-
icon: getIcon(s.provider)
309-
}));
304+
.map(s => {
305+
const iconInfo = getIconInfo(s.provider);
306+
return {
307+
id: s.provider,
308+
label: s.displayName,
309+
count: s.files.length,
310+
icon: iconInfo.path,
311+
iconNeedsInvert: iconInfo.needsInvert
312+
};
313+
});
310314
}, [sections]);
311315

312316
// Ensure active tab is valid (defaults to first available if current empty/invalid)

0 commit comments

Comments
 (0)