aura dark theme for Goose (#10283)
Co-authored-by: kursad <kursadk@neoplecxus.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Moon, Sliders, Sun } from 'lucide-react';
|
||||
import { Moon, Sliders, Sparkles, Sun } from 'lucide-react';
|
||||
import { Button } from '../ui/button';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { defineMessages, useIntl } from '../../i18n';
|
||||
@@ -17,6 +17,10 @@ const i18n = defineMessages({
|
||||
id: 'themeSelector.dark',
|
||||
defaultMessage: 'Dark',
|
||||
},
|
||||
aura: {
|
||||
id: 'themeSelector.aura',
|
||||
defaultMessage: 'Aura',
|
||||
},
|
||||
system: {
|
||||
id: 'themeSelector.system',
|
||||
defaultMessage: 'System',
|
||||
@@ -41,7 +45,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">{intl.formatMessage(i18n.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"
|
||||
@@ -73,6 +77,21 @@ const ThemeSelector: React.FC<ThemeSelectorProps> = ({
|
||||
<span>{intl.formatMessage(i18n.dark)}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
data-testid="aura-mode-button"
|
||||
onClick={() => setUserThemePreference('aura')}
|
||||
className={`flex items-center justify-center gap-1 p-2 rounded-md border transition-colors text-xs ${
|
||||
userThemePreference === 'aura'
|
||||
? '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"
|
||||
>
|
||||
<Sparkles className="h-3 w-3" />
|
||||
<span>{intl.formatMessage(i18n.aura)}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
data-testid="system-mode-button"
|
||||
onClick={() => setUserThemePreference('system')}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
||||
import { applyThemeTokens, buildMcpHostStyles } from '../theme/theme-tokens';
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { applyThemeTokens, buildMcpHostStyles, themes } from '../theme/theme-tokens';
|
||||
import type { ThemeId, ThemeVariant } 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' | 'aura' | 'system';
|
||||
type ResolvedTheme = ThemeVariant;
|
||||
|
||||
interface ThemeContextValue {
|
||||
userThemePreference: ThemePreference;
|
||||
setUserThemePreference: (pref: ThemePreference) => void;
|
||||
resolvedThemeId: ThemeId;
|
||||
resolvedTheme: ResolvedTheme;
|
||||
mcpHostStyles: McpUiHostStyles;
|
||||
}
|
||||
@@ -18,7 +20,9 @@ function getSystemTheme(): ResolvedTheme {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function resolveTheme(preference: ThemePreference): ResolvedTheme {
|
||||
// Resolve a user preference to a concrete theme id. 'system' picks the light or
|
||||
// dark built-in from the OS; named themes (light/dark/aura) map to themselves.
|
||||
function resolveThemeId(preference: ThemePreference): ThemeId {
|
||||
if (preference === 'system') {
|
||||
return getSystemTheme();
|
||||
}
|
||||
@@ -32,9 +36,6 @@ function applyThemeToDocument(theme: ResolvedTheme): void {
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
}
|
||||
|
||||
// Built once — light-dark() values are theme-independent
|
||||
const mcpHostStyles = buildMcpHostStyles();
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -42,7 +43,9 @@ interface ThemeProviderProps {
|
||||
export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
// Start with light theme to avoid flash, will update once settings load
|
||||
const [userThemePreference, setUserThemePreferenceState] = useState<ThemePreference>('light');
|
||||
const [resolvedTheme, setResolvedTheme] = useState<ResolvedTheme>('light');
|
||||
const [resolvedThemeId, setResolvedThemeId] = useState<ThemeId>('light');
|
||||
const resolvedTheme = themes[resolvedThemeId].variant;
|
||||
const mcpHostStyles = useMemo(() => buildMcpHostStyles(resolvedThemeId), [resolvedThemeId]);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadThemeFromSettings() {
|
||||
@@ -52,15 +55,10 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
window.electron.getSetting('theme'),
|
||||
]);
|
||||
|
||||
let preference: ThemePreference;
|
||||
if (useSystemTheme) {
|
||||
preference = 'system';
|
||||
} else {
|
||||
preference = savedTheme;
|
||||
}
|
||||
const preference: ThemePreference = useSystemTheme ? 'system' : savedTheme;
|
||||
|
||||
setUserThemePreferenceState(preference);
|
||||
setResolvedTheme(resolveTheme(preference));
|
||||
setResolvedThemeId(resolveThemeId(preference));
|
||||
} catch (error) {
|
||||
console.warn('[ThemeContext] Failed to load theme settings:', error);
|
||||
}
|
||||
@@ -72,8 +70,8 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
const setUserThemePreference = useCallback(async (preference: ThemePreference) => {
|
||||
setUserThemePreferenceState(preference);
|
||||
|
||||
const resolved = resolveTheme(preference);
|
||||
setResolvedTheme(resolved);
|
||||
const resolvedId = resolveThemeId(preference);
|
||||
setResolvedThemeId(resolvedId);
|
||||
|
||||
// Save to settings
|
||||
try {
|
||||
@@ -89,9 +87,9 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
|
||||
// Broadcast to other windows via Electron
|
||||
window.electron?.broadcastThemeChange({
|
||||
mode: resolved,
|
||||
mode: themes[resolvedId].variant,
|
||||
useSystemTheme: preference === 'system',
|
||||
theme: resolved,
|
||||
theme: resolvedId,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -102,7 +100,7 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handleChange = () => {
|
||||
setResolvedTheme(getSystemTheme());
|
||||
setResolvedThemeId(getSystemTheme());
|
||||
};
|
||||
|
||||
mediaQuery.addEventListener('change', handleChange);
|
||||
@@ -114,15 +112,13 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
if (!window.electron) return;
|
||||
|
||||
const handleThemeChanged = (_event: unknown, ...args: unknown[]) => {
|
||||
const themeData = args[0] as { useSystemTheme: boolean; theme: string };
|
||||
const themeData = args[0] as { useSystemTheme: boolean; theme: ThemeId };
|
||||
const newPreference: ThemePreference = themeData.useSystemTheme
|
||||
? 'system'
|
||||
: themeData.theme === 'dark'
|
||||
? 'dark'
|
||||
: 'light';
|
||||
: themeData.theme;
|
||||
|
||||
setUserThemePreferenceState(newPreference);
|
||||
setResolvedTheme(resolveTheme(newPreference));
|
||||
setResolvedThemeId(resolveThemeId(newPreference));
|
||||
|
||||
// Save to settings (don't await, fire and forget)
|
||||
if (newPreference === 'system') {
|
||||
@@ -139,15 +135,17 @@ export function ThemeProvider({ children }: ThemeProviderProps) {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Apply theme class and CSS tokens whenever resolvedTheme changes
|
||||
// Apply theme class and CSS tokens whenever the resolved theme changes
|
||||
useEffect(() => {
|
||||
applyThemeToDocument(resolvedTheme);
|
||||
applyThemeTokens(resolvedTheme);
|
||||
}, [resolvedTheme]);
|
||||
applyThemeToDocument(themes[resolvedThemeId].variant);
|
||||
applyThemeTokens(resolvedThemeId);
|
||||
document.documentElement.dataset.theme = resolvedThemeId;
|
||||
}, [resolvedThemeId]);
|
||||
|
||||
const value: ThemeContextValue = {
|
||||
userThemePreference,
|
||||
setUserThemePreference,
|
||||
resolvedThemeId,
|
||||
resolvedTheme,
|
||||
mcpHostStyles,
|
||||
};
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Telemetrie-Einstellungen konnten nicht aktualisiert werden."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Dunkel"
|
||||
},
|
||||
|
||||
@@ -4469,6 +4469,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Failed to update telemetry settings."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Dark"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "No se pudieron actualizar los ajustes de telemetría."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Oscuro"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Échec de la mise à jour des paramètres de télémétrie."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Sombre"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "टेलीमेट्री सेटिंग अपडेट करने में विफल."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "अंधेरा"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Gagal memperbarui pengaturan telemetri."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Gelap"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Impossibile aggiornare le impostazioni di telemetria."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Scuro"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "テレメトリ設定を更新できませんでした。"
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "ダーク"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "원격 분석 설정을 업데이트하지 못했습니다."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "다크"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Gagal mengemas kini tetapan telemetri."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Gelap"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Falha ao atualizar as definições de telemetria."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Escuro"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Не удалось обновить настройки телеметрии."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Темная"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Telemetri ayarları güncellenemedi."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Karanlık"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "Không thể cập nhật cài đặt đo từ xa."
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "Tối"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "更新遥测设置失败。"
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "深色"
|
||||
},
|
||||
|
||||
@@ -4439,6 +4439,9 @@
|
||||
"telemetrySettings.updateError": {
|
||||
"defaultMessage": "無法更新遙測設定。"
|
||||
},
|
||||
"themeSelector.aura": {
|
||||
"defaultMessage": "Aura"
|
||||
},
|
||||
"themeSelector.dark": {
|
||||
"defaultMessage": "深色"
|
||||
},
|
||||
|
||||
@@ -979,3 +979,13 @@ pre:has(> code.bg-inline-code) {
|
||||
.mcp-app-container.mcp-enter-inline {
|
||||
animation: mcp-enter-inline 180ms cubic-bezier(0.2, 0, 0, 1) both;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
AURA THEME — component overrides
|
||||
User message bubbles use the inverted bg-text-primary surface,
|
||||
which turns light in Aura. Restore Aura's dark chat bubble.
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
[data-theme='aura'] .user-message-bubble {
|
||||
background-color: #393647;
|
||||
color: #edecee;
|
||||
}
|
||||
|
||||
@@ -200,11 +200,96 @@ 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)',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Aura theme — colors & shadows (dark variant)
|
||||
// Ported from OpenChamber's Aura preset: purple-black surfaces, purple accent,
|
||||
// mint/peach/cyan/coral status colors, monospace typography.
|
||||
// ---------------------------------------------------------------------------
|
||||
const auraColorTokens: ColorTokens = {
|
||||
// Backgrounds
|
||||
'--color-background-primary': '#15141b',
|
||||
'--color-background-secondary': '#1a1921',
|
||||
'--color-background-tertiary': '#201e2b',
|
||||
'--color-background-inverse': '#a277ff',
|
||||
'--color-background-ghost': 'transparent',
|
||||
'--color-background-info': '#82e2ff',
|
||||
'--color-background-danger': '#ff6767',
|
||||
'--color-background-success': '#61ffca',
|
||||
'--color-background-warning': '#ffca85',
|
||||
'--color-background-disabled': '#25232f',
|
||||
|
||||
// Text
|
||||
'--color-text-primary': '#edecee',
|
||||
'--color-text-secondary': '#8a8282',
|
||||
'--color-text-tertiary': '#6d6d6d',
|
||||
'--color-text-inverse': '#15141b',
|
||||
'--color-text-ghost': '#8a8282',
|
||||
'--color-text-info': '#82e2ff',
|
||||
'--color-text-danger': '#ff6767',
|
||||
'--color-text-success': '#61ffca',
|
||||
'--color-text-warning': '#ffca85',
|
||||
'--color-text-disabled': '#525b68',
|
||||
|
||||
// Borders
|
||||
'--color-border-primary': '#2d2b38',
|
||||
'--color-border-secondary': '#47415a',
|
||||
'--color-border-tertiary': '#4e496c',
|
||||
'--color-border-inverse': '#edecee',
|
||||
'--color-border-ghost': 'transparent',
|
||||
'--color-border-info': '#82e2ff',
|
||||
'--color-border-danger': '#ff6767',
|
||||
'--color-border-success': '#61ffca',
|
||||
'--color-border-warning': '#ffca85',
|
||||
'--color-border-disabled': '#2d2b38',
|
||||
|
||||
// Rings
|
||||
'--color-ring-primary': '#47415a',
|
||||
'--color-ring-secondary': '#2d2b38',
|
||||
'--color-ring-inverse': '#15141b',
|
||||
'--color-ring-info': '#82e2ff',
|
||||
'--color-ring-danger': '#ff6767',
|
||||
'--color-ring-success': '#61ffca',
|
||||
'--color-ring-warning': '#ffca85',
|
||||
|
||||
// Shadows (dark)
|
||||
'--shadow-hairline': '0 0 0 1px rgba(0, 0, 0, 0.2)',
|
||||
'--shadow-sm': '0 1px 2px 0 rgba(0, 0, 0, 0.2)',
|
||||
'--shadow-md': '0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -2px rgba(0, 0, 0, 0.2)',
|
||||
'--shadow-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -4px rgba(0, 0, 0, 0.2)',
|
||||
};
|
||||
|
||||
// Aura is monospace-first — override the shared sans family.
|
||||
const auraFontTokens: Partial<Pick<ThemeTokens, BaseTokenKey>> = {
|
||||
'--font-sans': 'ui-monospace, "SFMono-Regular", "Menlo", "Cascadia Mono", "Segoe UI Mono", monospace',
|
||||
'--font-mono': 'ui-monospace, "SFMono-Regular", "Menlo", "Cascadia Mono", "Segoe UI Mono", monospace',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Merged token maps — used by applyThemeTokens() and buildMcpHostStyles()
|
||||
// ---------------------------------------------------------------------------
|
||||
export const lightTokens: ThemeTokens = { ...baseTokens, ...lightColorTokens };
|
||||
export const darkTokens: ThemeTokens = { ...baseTokens, ...darkColorTokens };
|
||||
export const auraTokens: ThemeTokens = { ...baseTokens, ...auraFontTokens, ...auraColorTokens };
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme registry — the set of selectable named themes.
|
||||
// `variant` drives the .dark/.light class and colorScheme for anything outside
|
||||
// the token system; `tokens` is the map applied to :root. Adding a future theme
|
||||
// is a single entry here plus its token map above.
|
||||
// ---------------------------------------------------------------------------
|
||||
export type ThemeId = 'light' | 'dark' | 'aura';
|
||||
export type ThemeVariant = 'light' | 'dark';
|
||||
|
||||
interface ThemeDefinition {
|
||||
variant: ThemeVariant;
|
||||
tokens: ThemeTokens;
|
||||
}
|
||||
|
||||
export const themes: Record<ThemeId, ThemeDefinition> = {
|
||||
light: { variant: 'light', tokens: lightTokens },
|
||||
dark: { variant: 'dark', tokens: darkTokens },
|
||||
aura: { variant: 'dark', tokens: auraTokens },
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
@@ -244,43 +329,51 @@ const HOST_FONT_CSS = `
|
||||
|
||||
/**
|
||||
* Build the McpUiHostStyles object for MCP apps.
|
||||
* Color keys use light-dark() so a single payload works for both themes.
|
||||
* Non-color keys (fonts, radii, shadows) use plain values from baseTokens
|
||||
* (or light as the default when values differ, e.g. shadows).
|
||||
*
|
||||
* For the built-in light/dark pair, color keys use light-dark() so a single
|
||||
* payload resolves correctly against the guest's color-scheme. Custom themes
|
||||
* (e.g. aura) share a variant with dark but carry their own palette and fonts,
|
||||
* so light-dark() can't express them — emit that theme's concrete token values
|
||||
* instead. Non-color keys always use the theme's own values so overrides like
|
||||
* Aura's monospace font family reach the guest.
|
||||
* css.fonts provides @font-face rules so sandboxed apps can load host fonts.
|
||||
*/
|
||||
export function buildMcpHostStyles(): McpUiHostStyles {
|
||||
export function buildMcpHostStyles(themeId: ThemeId = 'light'): McpUiHostStyles {
|
||||
const tokens = (themes[themeId] ?? themes.light).tokens;
|
||||
const isBuiltinVariant = themeId === 'light' || themeId === 'dark';
|
||||
const variables: McpUiStyles = {} as McpUiStyles;
|
||||
for (const key of Object.keys(lightTokens) as McpUiStyleVariableKey[]) {
|
||||
const light = lightTokens[key];
|
||||
const dark = darkTokens[key];
|
||||
if (key.startsWith('--color-')) {
|
||||
variables[key] = `light-dark(${light}, ${dark})`;
|
||||
if (key.startsWith('--color-') && isBuiltinVariant) {
|
||||
variables[key] = `light-dark(${lightTokens[key]}, ${darkTokens[key]})`;
|
||||
} else {
|
||||
variables[key] = light;
|
||||
variables[key] = tokens[key];
|
||||
}
|
||||
}
|
||||
return { variables, css: { fonts: HOST_FONT_CSS } };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the current theme from localStorage / system preference.
|
||||
* Resolve the current theme id from localStorage / system preference.
|
||||
* Best-effort pre-paint resolution; the authoritative preference lives in the
|
||||
* Electron settings store and is applied by ThemeContext once loaded.
|
||||
*/
|
||||
export function getResolvedTheme(): 'light' | 'dark' {
|
||||
export function getResolvedTheme(): ThemeId {
|
||||
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 stored = localStorage.getItem('theme');
|
||||
if (stored === 'aura') return 'aura';
|
||||
return stored === 'dark' ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply theme tokens to the document root as CSS custom properties.
|
||||
* Apply a theme's 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?: ThemeId): void {
|
||||
const resolved = theme ?? getResolvedTheme();
|
||||
const tokens = resolved === 'dark' ? darkTokens : lightTokens;
|
||||
const { tokens } = themes[resolved] ?? themes.light;
|
||||
const root = document.documentElement;
|
||||
for (const [key, value] of Object.entries(tokens)) {
|
||||
root.style.setProperty(key, value);
|
||||
|
||||
@@ -48,7 +48,7 @@ export interface Settings {
|
||||
keyboardShortcuts: KeyboardShortcuts;
|
||||
|
||||
// UI preferences (migrated from localStorage)
|
||||
theme: 'dark' | 'light';
|
||||
theme: 'dark' | 'light' | 'aura';
|
||||
useSystemTheme: boolean;
|
||||
language: LanguageSetting;
|
||||
responseStyle: string;
|
||||
|
||||
Reference in New Issue
Block a user