diff --git a/ui/desktop/index.html b/ui/desktop/index.html index 78a85db070c2..6547abc5d63f 100644 --- a/ui/desktop/index.html +++ b/ui/desktop/index.html @@ -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 { diff --git a/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx b/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx index f3783bd753e9..6a1770432a17 100644 --- a/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx +++ b/ui/desktop/src/components/GooseSidebar/ThemeSelector.tsx @@ -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'; @@ -20,7 +20,7 @@ const ThemeSelector: React.FC = ({ {!hideTitle && Theme} = ({ Dark + + 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" + > + + Glass + + setUserThemePreference('system')} diff --git a/ui/desktop/src/contexts/ThemeContext.tsx b/ui/desktop/src/contexts/ThemeContext.tsx index 16c97e7bda68..72141bf196f8 100644 --- a/ui/desktop/src/contexts/ThemeContext.tsx +++ b/ui/desktop/src/contexts/ThemeContext.tsx @@ -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; @@ -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 @@ -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); @@ -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)); diff --git a/ui/desktop/src/styles/main.css b/ui/desktop/src/styles/main.css index f4f96837f9df..d29e1de2efdb 100644 --- a/ui/desktop/src/styles/main.css +++ b/ui/desktop/src/styles/main.css @@ -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); +} diff --git a/ui/desktop/src/theme/theme-tokens.ts b/ui/desktop/src/theme/theme-tokens.ts index fa1d2d5d622a..4f7d9a46177f 100644 --- a/ui/desktop/src/theme/theme-tokens.ts +++ b/ui/desktop/src/theme/theme-tokens.ts @@ -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 @@ -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);