Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions ui/desktop/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@
const useSystemTheme = localStorage.getItem('use_system_theme') === 'true';
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const savedTheme = localStorage.getItem('theme');
const isDark = useSystemTheme ? systemPrefersDark : (savedTheme ? savedTheme === 'dark' : systemPrefersDark);
const isGlass = !useSystemTheme && savedTheme === 'glass';
const isDark = useSystemTheme ? systemPrefersDark : (savedTheme ? savedTheme === 'dark' || savedTheme === 'glass' : systemPrefersDark);

if (isDark) {
if (isGlass) {
document.documentElement.classList.add('dark', 'glass');
document.documentElement.style.colorScheme = 'dark';
} else if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('glass');
document.documentElement.style.colorScheme = 'dark';
} else {
document.documentElement.classList.remove('dark');
document.documentElement.classList.remove('dark', 'glass');
document.documentElement.style.colorScheme = 'light';
}
} else {
Expand Down
20 changes: 18 additions & 2 deletions ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Moon, Sliders, Sun } from 'lucide-react';
import { Layers, Moon, Sliders, Sun } from 'lucide-react';
import { Button } from '../ui/button';
import { useTheme } from '../../contexts/ThemeContext';

Expand All @@ -20,7 +20,7 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({
<div className={`${!horizontal ? 'px-1 py-2 space-y-2' : ''} ${className}`}>
{!hideTitle && <div className="text-xs text-text-primary px-3">Theme</div>}
<div
className={`${horizontal ? 'flex' : 'grid grid-cols-3'} gap-1 ${!horizontal ? 'px-3' : ''}`}
className={`${horizontal ? 'flex' : 'grid grid-cols-4'} gap-1 ${!horizontal ? 'px-3' : ''}`}
>
<Button
data-testid="light-mode-button"
Expand Down Expand Up @@ -52,6 +52,22 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({
<span>Dark</span>
</Button>


<Button
data-testid="glass-mode-button"
onClick={() => setUserThemePreference('glass')}
className={`flex items-center justify-center gap-1 p-2 rounded-md border transition-colors text-xs ${
userThemePreference === 'glass'
? 'bg-background-inverse text-text-inverse border-text-inverse hover:!bg-background-inverse hover:!text-text-inverse'
: 'border-border-primary hover:!bg-background-secondary text-text-secondary hover:text-text-primary'
}`}
variant="ghost"
size="sm"
>
<Layers className="h-3 w-3" />
<span>Glass</span>
</Button>

<Button
data-testid="system-mode-button"
onClick={() => setUserThemePreference('system')}
Expand Down
30 changes: 21 additions & 9 deletions ui/desktop/src/contexts/ThemeContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import React, { createContext, useContext, useEffect, useState, useCallback } fr
import { applyThemeTokens, buildMcpHostStyles } from '../theme/theme-tokens';
import type { McpUiHostStyles } from '@modelcontextprotocol/ext-apps/app-bridge';

type ThemePreference = 'light' | 'dark' | 'system';
type ResolvedTheme = 'light' | 'dark';
type ThemePreference = 'light' | 'dark' | 'glass' | 'system';
type ResolvedTheme = 'light' | 'dark' | 'glass';

interface ThemeContextValue {
userThemePreference: ThemePreference;
Expand All @@ -25,11 +25,19 @@ function resolveTheme(preference: ThemePreference): ResolvedTheme {
return preference;
}

function getThemeClass(theme: ResolvedTheme): string {
// Glass uses dark-mode text/UI conventions
return theme === 'glass' ? 'dark' : theme;
}

function applyThemeToDocument(theme: ResolvedTheme): void {
const toRemove = theme === 'dark' ? 'light' : 'dark';
document.documentElement.classList.add(theme);
const themeClass = getThemeClass(theme);
const toRemove = themeClass === 'dark' ? 'light' : 'dark';
document.documentElement.classList.add(themeClass);
document.documentElement.classList.remove(toRemove);
document.documentElement.style.colorScheme = theme;
// Toggle glass class for backdrop-filter styles
document.documentElement.classList.toggle('glass', theme === 'glass');
document.documentElement.style.colorScheme = themeClass;
}

// Built once — light-dark() values are theme-independent
Expand All @@ -55,8 +63,10 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
let preference: ThemePreference;
if (useSystemTheme) {
preference = 'system';
} else {
} else if (savedTheme === 'glass' || savedTheme === 'dark' || savedTheme === 'light') {
preference = savedTheme;
} else {
preference = 'light';
}

setUserThemePreferenceState(preference);
Expand Down Expand Up @@ -117,9 +127,11 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
const themeData = args[0] as { useSystemTheme: boolean; theme: string };
const newPreference: ThemePreference = themeData.useSystemTheme
? 'system'
: themeData.theme === 'dark'
? 'dark'
: 'light';
: themeData.theme === 'glass'
? 'glass'
: themeData.theme === 'dark'
? 'dark'
: 'light';

setUserThemePreferenceState(newPreference);
setResolvedTheme(resolveTheme(newPreference));
Expand Down
55 changes: 55 additions & 0 deletions ui/desktop/src/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -927,3 +927,58 @@ p > code.bg-inline-code {
.mcp-app-container.mcp-enter-inline {
animation: mcp-enter-inline 180ms cubic-bezier(0.2, 0, 0, 1) both;
}

/* ═══════════════════════════════════════════════════════════════════════════
GLASS THEME — backdrop-filter for native vibrancy blur-through
Applied when html.glass is set by ThemeContext.
On macOS, Electron's vibrancy: 'window' provides the native blur layer;
these styles make the CSS backgrounds transparent enough to reveal it.
On other platforms, backdrop-filter provides a CSS-only approximation.
═══════════════════════════════════════════════════════════════════════════ */

html.glass body {
background-color: transparent;
}

html.glass #root {
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
}

.glass {
/* Legacy aliases */
--text-inverse: var(--color-black);
--text-danger: var(--color-red-100);

/* Search highlighting */
--highlight-color: rgba(255, 213, 0, 0.4);
--highlight-current: rgba(252, 213, 3, 0.5);

/* Sidebar aliases — semi-transparent for glass effect */
--sidebar: var(--color-background-secondary);
--sidebar-foreground: var(--color-text-primary);
--sidebar-primary: var(--color-background-inverse);
--sidebar-primary-foreground: var(--color-text-inverse);
--sidebar-accent: var(--color-background-secondary);
--sidebar-accent-foreground: var(--color-text-primary);
--sidebar-border: var(--color-border-primary);
--sidebar-ring: var(--color-border-primary);

/* Custom shadow — softer for glass */
--shadow-default:
0px 12px 32px 0px rgba(0, 0, 0, 0.15), 0px 8px 16px 0px rgba(0, 0, 0, 0.1),
0px 2px 4px 0px rgba(0, 0, 0, 0.08), 0px 0px 1px 0px rgba(255, 255, 255, 0.1);
}

/* Glass scrollbar — more transparent */
.glass * {
scrollbar-color: rgba(255, 255, 255, 0.15) transparent;
}

.glass ::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.15);
}

.glass ::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.25);
}
68 changes: 64 additions & 4 deletions ui/desktop/src/theme/theme-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,69 @@ const darkColorTokens: ColorTokens = {
'--shadow-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.2)',
};


// ---------------------------------------------------------------------------
// Glass theme — semi-transparent colors for native vibrancy blur-through
// ---------------------------------------------------------------------------
const glassColorTokens: ColorTokens = {
// Backgrounds — semi-transparent to let native vibrancy show through
'--color-background-primary': 'rgba(30, 30, 30, 0.55)',
'--color-background-secondary': 'rgba(50, 50, 50, 0.45)',
'--color-background-tertiary': 'rgba(70, 70, 70, 0.35)',
'--color-background-inverse': 'rgba(205, 209, 214, 0.9)',
'--color-background-ghost': 'transparent',
'--color-background-info': '#7cacff',
'--color-background-danger': '#ff6b6b',
'--color-background-success': '#a3d795',
'--color-background-warning': '#ffd966',
'--color-background-disabled': 'rgba(71, 78, 87, 0.4)',

// Text — fully opaque for readability against translucent backgrounds
'--color-text-primary': '#ffffff',
'--color-text-secondary': '#a0a0a0',
'--color-text-tertiary': '#707a86',
'--color-text-inverse': '#000000',
'--color-text-ghost': '#a0a0a0',
'--color-text-info': '#7cacff',
'--color-text-danger': '#ff6b6b',
'--color-text-success': '#a3d795',
'--color-text-warning': '#ffd966',
'--color-text-disabled': '#525b68',

// Borders — subtle, semi-transparent
'--color-border-primary': 'rgba(255, 255, 255, 0.1)',
'--color-border-secondary': 'rgba(255, 255, 255, 0.15)',
'--color-border-tertiary': 'rgba(255, 255, 255, 0.08)',
'--color-border-inverse': '#ffffff',
'--color-border-ghost': 'transparent',
'--color-border-info': '#7cacff',
'--color-border-danger': '#ff6b6b',
'--color-border-success': '#a3d795',
'--color-border-warning': '#ffd966',
'--color-border-disabled': 'rgba(255, 255, 255, 0.05)',

// Rings — semi-transparent
'--color-ring-primary': 'rgba(255, 255, 255, 0.15)',
'--color-ring-secondary': 'rgba(255, 255, 255, 0.1)',
'--color-ring-inverse': '#000000',
'--color-ring-info': '#7cacff',
'--color-ring-danger': '#ff6b6b',
'--color-ring-success': '#a3d795',
'--color-ring-warning': '#ffd966',

// Shadows — softer, with slight glow
'--shadow-hairline': '0 0 0 1px rgba(255, 255, 255, 0.05)',
'--shadow-sm': '0 1px 2px 0 rgba(0, 0, 0, 0.15)',
'--shadow-md': '0 4px 6px -1px rgba(0, 0, 0, 0.2), 0 2px 4px -2px rgba(0, 0, 0, 0.15)',
'--shadow-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.25), 0 4px 6px -4px rgba(0, 0, 0, 0.15)',
};

// ---------------------------------------------------------------------------
// Merged token maps — used by applyThemeTokens() and buildMcpHostStyles()
// ---------------------------------------------------------------------------
export const lightTokens: ThemeTokens = { ...baseTokens, ...lightColorTokens };
export const darkTokens: ThemeTokens = { ...baseTokens, ...darkColorTokens };
export const glassTokens: ThemeTokens = { ...baseTokens, ...glassColorTokens };

// ---------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -266,21 +324,23 @@ export function buildMcpHostStyles(): McpUiHostStyles {
/**
* Resolve the current theme from localStorage / system preference.
*/
export function getResolvedTheme(): 'light' | 'dark' {
export function getResolvedTheme(): 'light' | 'dark' | 'glass' {
const useSystem = localStorage.getItem('use_system_theme') !== 'false';
if (useSystem) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light';
const saved = localStorage.getItem('theme');
if (saved === 'glass') return 'glass';
return saved === 'dark' ? 'dark' : 'light';
}

/**
* Apply theme tokens to the document root as CSS custom properties.
* When called without an argument, resolves the theme from localStorage.
*/
export function applyThemeTokens(theme?: 'light' | 'dark'): void {
export function applyThemeTokens(theme?: 'light' | 'dark' | 'glass'): void {
const resolved = theme ?? getResolvedTheme();
const tokens = resolved === 'dark' ? darkTokens : lightTokens;
const tokens = resolved === 'glass' ? glassTokens : resolved === 'dark' ? darkTokens : lightTokens;
const root = document.documentElement;
for (const [key, value] of Object.entries(tokens)) {
root.style.setProperty(key, value);
Expand Down
Loading