feat: introduced a token-based theming system across the UI
* feat: added themes system * feat: smart sidebar auto-hide for files/diff tabs + lower files sidebar threshold * feat: added Checkbox component and update theming - Add reusable Checkbox component for toggles across UI - Replace several inputs with Checkbox in settings and commands panels - Add DiffIcon and apply surface/border theming to key UI areas * feat: Add convert-vscode-theme.cjs to convert VS Code themes to OpenChamber format * refactor: remove unused permission logic from ChatInput - Remove unused permission rules parsing logic from ChatInput - Memoize renderTheme in DiffWorkerProvider to avoid unnecessary recalculations - Remove forceOpaque helper in vscode theme adapter * fix: guard VSCode theme loading in MarkdownRenderer * feat: add custom user themes loading and reload - Load user themes from ~/.config/openchamber/themes at runtime - Expose /api/config/themes to fetch custom themes - Allow theme reloading from Settings → Theme → Reload themes in the UI
This commit is contained in:
committed by
GitHub
parent
5bb2c56bc0
commit
ddedc02687
@@ -5,8 +5,8 @@ import type { SupportedLanguages } from '@pierre/diffs';
|
||||
|
||||
import { useOptionalThemeSystem } from './useThemeSystem';
|
||||
import { workerFactory } from '@/lib/diff/workerFactory';
|
||||
import { ensureFlexokiThemesRegistered } from '@/lib/shiki/registerFlexokiThemes';
|
||||
import { flexokiThemeNames } from '@/lib/shiki/flexokiThemes';
|
||||
import { ensurePierreThemeRegistered } from '@/lib/shiki/appThemeRegistry';
|
||||
import { getDefaultTheme } from '@/lib/theme/themes';
|
||||
import { useGitStore } from '@/stores/useGitStore';
|
||||
import { getLanguageFromExtension } from '@/lib/toolHelpers';
|
||||
|
||||
@@ -31,14 +31,14 @@ const PRELOAD_LANGS: SupportedLanguages[] = [
|
||||
const WARMUP_MAX_FILES = 10;
|
||||
|
||||
// Matches cache key logic in `packages/ui/src/components/views/PierreDiffViewer.tsx`
|
||||
function getPierreCacheKey(fileName: string, original: string, modified: string): string {
|
||||
function getPierreCacheKey(fileName: string, original: string, modified: string, themeKey: string): string {
|
||||
const sampleOriginal = original.length > 100
|
||||
? `${original.slice(0, 50)}${original.slice(-50)}`
|
||||
: original;
|
||||
const sampleModified = modified.length > 100
|
||||
? `${modified.slice(0, 50)}${modified.slice(-50)}`
|
||||
: modified;
|
||||
return `${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
||||
return `${themeKey}::${fileName}:${original.length}:${modified.length}:${sampleOriginal.length}:${sampleModified.length}`;
|
||||
}
|
||||
|
||||
interface DiffWorkerProviderProps {
|
||||
@@ -57,7 +57,11 @@ function scheduleWarmupWork(cb: (deadline?: IdleDeadlineLike) => void): () => vo
|
||||
}
|
||||
|
||||
// Component that warms up the worker pool and precomputes diff ASTs
|
||||
const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const WorkerPoolWarmup: React.FC<{
|
||||
children: React.ReactNode;
|
||||
themeKey: string;
|
||||
renderTheme: { light: string; dark: string };
|
||||
}> = ({ children, themeKey, renderTheme }) => {
|
||||
const workerPool = useWorkerPool();
|
||||
const activeDirectory = useGitStore((state) => state.activeDirectory);
|
||||
const lastStatusChange = useGitStore((state) => {
|
||||
@@ -72,6 +76,20 @@ const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children })
|
||||
const didDummyWarmupRef = useRef(false);
|
||||
const warmedStatusRef = useRef(new Map<string, number>());
|
||||
|
||||
useEffect(() => {
|
||||
warmedStatusRef.current.clear();
|
||||
}, [themeKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workerPool) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Important: WorkerPoolContextProvider uses a singleton and does not react to
|
||||
// prop changes. Update the worker pool render options explicitly.
|
||||
void workerPool.setRenderOptions({ theme: renderTheme });
|
||||
}, [renderTheme, workerPool]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workerPool || didDummyWarmupRef.current) return;
|
||||
|
||||
@@ -128,7 +146,7 @@ const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children })
|
||||
index += 1;
|
||||
|
||||
const language = getLanguageFromExtension(filePath) || 'text';
|
||||
const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified);
|
||||
const cacheKey = getPierreCacheKey(filePath, diff.original, diff.modified, themeKey);
|
||||
|
||||
const oldFile: FileContents = {
|
||||
name: filePath,
|
||||
@@ -171,7 +189,7 @@ const WorkerPoolWarmup: React.FC<{ children: React.ReactNode }> = ({ children })
|
||||
cancelled = true;
|
||||
cancelScheduled?.();
|
||||
};
|
||||
}, [workerPool, activeDirectory, lastStatusChange, diffCacheSize]);
|
||||
}, [workerPool, activeDirectory, lastStatusChange, diffCacheSize, themeKey]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -180,16 +198,40 @@ export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children
|
||||
const themeSystem = useOptionalThemeSystem();
|
||||
const isDark = themeSystem?.currentTheme?.metadata?.variant === 'dark';
|
||||
|
||||
ensureFlexokiThemesRegistered();
|
||||
const fallbackLight = getDefaultTheme(false);
|
||||
const fallbackDark = getDefaultTheme(true);
|
||||
|
||||
const lightThemeId = themeSystem?.lightThemeId ?? fallbackLight.metadata.id;
|
||||
const darkThemeId = themeSystem?.darkThemeId ?? fallbackDark.metadata.id;
|
||||
|
||||
const lightTheme =
|
||||
themeSystem?.availableThemes.find((theme) => theme.metadata.id === lightThemeId) ??
|
||||
fallbackLight;
|
||||
const darkTheme =
|
||||
themeSystem?.availableThemes.find((theme) => theme.metadata.id === darkThemeId) ??
|
||||
fallbackDark;
|
||||
|
||||
ensurePierreThemeRegistered(lightTheme);
|
||||
ensurePierreThemeRegistered(darkTheme);
|
||||
|
||||
const highlighterOptions = useMemo(() => ({
|
||||
theme: {
|
||||
dark: flexokiThemeNames.dark,
|
||||
light: flexokiThemeNames.light,
|
||||
dark: darkTheme.metadata.id,
|
||||
light: lightTheme.metadata.id,
|
||||
},
|
||||
themeType: isDark ? ('dark' as const) : ('light' as const),
|
||||
langs: PRELOAD_LANGS,
|
||||
}), [isDark]);
|
||||
}), [darkTheme.metadata.id, isDark, lightTheme.metadata.id]);
|
||||
|
||||
const workerThemeKey = `${lightTheme.metadata.id}:${darkTheme.metadata.id}:${isDark ? 'dark' : 'light'}`;
|
||||
|
||||
const renderTheme = useMemo(
|
||||
() => ({
|
||||
light: lightTheme.metadata.id,
|
||||
dark: darkTheme.metadata.id,
|
||||
}),
|
||||
[darkTheme.metadata.id, lightTheme.metadata.id],
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkerPoolContextProvider
|
||||
@@ -200,7 +242,10 @@ export const DiffWorkerProvider: React.FC<DiffWorkerProviderProps> = ({ children
|
||||
}}
|
||||
highlighterOptions={highlighterOptions}
|
||||
>
|
||||
<WorkerPoolWarmup>
|
||||
<WorkerPoolWarmup
|
||||
themeKey={workerThemeKey}
|
||||
renderTheme={renderTheme}
|
||||
>
|
||||
{children}
|
||||
</WorkerPoolWarmup>
|
||||
</WorkerPoolContextProvider>
|
||||
|
||||
@@ -6,18 +6,18 @@ import React, {
|
||||
} from 'react';
|
||||
import type { Theme, ThemeMode } from '@/types/theme';
|
||||
import type { DesktopSettings } from '@/lib/desktop';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { isDesktopRuntime, isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { CSSVariableGenerator } from '@/lib/theme/cssGenerator';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import {
|
||||
themes,
|
||||
getThemeById,
|
||||
flexokiLightTheme,
|
||||
flexokiDarkTheme,
|
||||
getDefaultTheme,
|
||||
DEFAULT_LIGHT_THEME_ID,
|
||||
DEFAULT_DARK_THEME_ID,
|
||||
} from '@/lib/theme/themes';
|
||||
import { ThemeSystemContext, type ThemeContextValue } from './theme-system-context';
|
||||
import type { VSCodeThemePayload } from '@/lib/theme/vscode/adapter';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
type ThemePreferences = {
|
||||
themeMode: ThemeMode;
|
||||
@@ -25,8 +25,8 @@ type ThemePreferences = {
|
||||
darkThemeId: string;
|
||||
};
|
||||
|
||||
const DEFAULT_LIGHT_ID = flexokiLightTheme.metadata.id;
|
||||
const DEFAULT_DARK_ID = flexokiDarkTheme.metadata.id;
|
||||
const DEFAULT_LIGHT_ID = DEFAULT_LIGHT_THEME_ID;
|
||||
const DEFAULT_DARK_ID = DEFAULT_DARK_THEME_ID;
|
||||
|
||||
const getSystemPreference = (): boolean => {
|
||||
if (typeof window === 'undefined') {
|
||||
@@ -36,38 +36,83 @@ const getSystemPreference = (): boolean => {
|
||||
};
|
||||
|
||||
const fallbackThemeForVariant = (variant: 'light' | 'dark'): Theme =>
|
||||
variant === 'dark' ? flexokiDarkTheme : flexokiLightTheme;
|
||||
|
||||
const findFallbackThemeId = (variant: 'light' | 'dark'): string => {
|
||||
const fallback = themes.find((candidate) => candidate.metadata.variant === variant);
|
||||
return (fallback ?? fallbackThemeForVariant(variant)).metadata.id;
|
||||
};
|
||||
|
||||
const ensureThemeById = (themeId: string, variant: 'light' | 'dark'): Theme => {
|
||||
const theme = getThemeById(themeId);
|
||||
if (theme && theme.metadata.variant === variant) {
|
||||
return theme;
|
||||
}
|
||||
const fallback = themes.find((candidate) => candidate.metadata.variant === variant);
|
||||
return fallback ?? fallbackThemeForVariant(variant);
|
||||
};
|
||||
getDefaultTheme(variant === 'dark');
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect;
|
||||
|
||||
const validateThemeId = (themeId: string | null, variant: 'light' | 'dark'): string => {
|
||||
if (!themeId) {
|
||||
return variant === 'light' ? DEFAULT_LIGHT_ID : DEFAULT_DARK_ID;
|
||||
const isNonEmptyString = (value: unknown): value is string =>
|
||||
typeof value === 'string' && value.trim().length > 0;
|
||||
|
||||
const getNested = (value: unknown, path: string[]): unknown =>
|
||||
path.reduce<unknown>((acc, key) => (acc && typeof acc === 'object' ? (acc as Record<string, unknown>)[key] : undefined), value);
|
||||
|
||||
const isValidCustomTheme = (value: unknown): value is Theme => {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false;
|
||||
}
|
||||
const theme = getThemeById(themeId);
|
||||
if (theme && theme.metadata.variant === variant) {
|
||||
return theme.metadata.id;
|
||||
|
||||
const requiredPaths = [
|
||||
['metadata', 'id'],
|
||||
['metadata', 'name'],
|
||||
['metadata', 'variant'],
|
||||
['colors', 'primary', 'base'],
|
||||
['colors', 'primary', 'foreground'],
|
||||
['colors', 'surface', 'background'],
|
||||
['colors', 'surface', 'foreground'],
|
||||
['colors', 'surface', 'muted'],
|
||||
['colors', 'surface', 'mutedForeground'],
|
||||
['colors', 'surface', 'elevated'],
|
||||
['colors', 'surface', 'elevatedForeground'],
|
||||
['colors', 'surface', 'subtle'],
|
||||
['colors', 'interactive', 'border'],
|
||||
['colors', 'interactive', 'selection'],
|
||||
['colors', 'interactive', 'selectionForeground'],
|
||||
['colors', 'interactive', 'focusRing'],
|
||||
['colors', 'interactive', 'hover'],
|
||||
['colors', 'status', 'error'],
|
||||
['colors', 'status', 'errorForeground'],
|
||||
['colors', 'status', 'errorBackground'],
|
||||
['colors', 'status', 'errorBorder'],
|
||||
['colors', 'status', 'warning'],
|
||||
['colors', 'status', 'warningForeground'],
|
||||
['colors', 'status', 'warningBackground'],
|
||||
['colors', 'status', 'warningBorder'],
|
||||
['colors', 'status', 'success'],
|
||||
['colors', 'status', 'successForeground'],
|
||||
['colors', 'status', 'successBackground'],
|
||||
['colors', 'status', 'successBorder'],
|
||||
['colors', 'status', 'info'],
|
||||
['colors', 'status', 'infoForeground'],
|
||||
['colors', 'status', 'infoBackground'],
|
||||
['colors', 'status', 'infoBorder'],
|
||||
['colors', 'syntax', 'base', 'background'],
|
||||
['colors', 'syntax', 'base', 'foreground'],
|
||||
['colors', 'syntax', 'base', 'keyword'],
|
||||
['colors', 'syntax', 'base', 'string'],
|
||||
['colors', 'syntax', 'base', 'number'],
|
||||
['colors', 'syntax', 'base', 'function'],
|
||||
['colors', 'syntax', 'base', 'variable'],
|
||||
['colors', 'syntax', 'base', 'type'],
|
||||
['colors', 'syntax', 'base', 'comment'],
|
||||
['colors', 'syntax', 'base', 'operator'],
|
||||
['colors', 'syntax', 'highlights', 'diffAdded'],
|
||||
['colors', 'syntax', 'highlights', 'diffRemoved'],
|
||||
['colors', 'syntax', 'highlights', 'lineNumber'],
|
||||
];
|
||||
|
||||
for (const path of requiredPaths) {
|
||||
if (!isNonEmptyString(getNested(value, path))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return findFallbackThemeId(variant);
|
||||
|
||||
const variant = getNested(value, ['metadata', 'variant']);
|
||||
return variant === 'light' || variant === 'dark';
|
||||
};
|
||||
|
||||
const buildInitialPreferences = (defaultThemeId?: string): ThemePreferences => {
|
||||
let lightThemeId = DEFAULT_LIGHT_ID;
|
||||
let darkThemeId = DEFAULT_DARK_ID;
|
||||
let lightThemeId: string = DEFAULT_LIGHT_ID;
|
||||
let darkThemeId: string = DEFAULT_DARK_ID;
|
||||
let themeMode: ThemeMode = 'system';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -99,8 +144,13 @@ const buildInitialPreferences = (defaultThemeId?: string): ThemePreferences => {
|
||||
themeMode = legacyVariant;
|
||||
}
|
||||
|
||||
lightThemeId = validateThemeId(storedLightId, 'light');
|
||||
darkThemeId = validateThemeId(storedDarkId, 'dark');
|
||||
if (typeof storedLightId === 'string' && storedLightId.trim().length > 0) {
|
||||
lightThemeId = storedLightId.trim();
|
||||
}
|
||||
|
||||
if (typeof storedDarkId === 'string' && storedDarkId.trim().length > 0) {
|
||||
darkThemeId = storedDarkId.trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultThemeId) {
|
||||
@@ -130,6 +180,8 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
const cssGenerator = useMemo(() => new CSSVariableGenerator(), []);
|
||||
const [preferences, setPreferences] = useState<ThemePreferences>(() => buildInitialPreferences(defaultThemeId));
|
||||
const [systemPrefersDark, setSystemPrefersDark] = useState<boolean>(() => getSystemPreference());
|
||||
const [customThemes, setCustomThemes] = useState<Theme[]>([]);
|
||||
const [customThemesLoading, setCustomThemesLoading] = useState(false);
|
||||
const [vscodeTheme, setVSCodeTheme] = useState<Theme | null>(() => {
|
||||
if (typeof window === 'undefined' || !isVSCodeRuntime()) {
|
||||
return null;
|
||||
@@ -138,6 +190,47 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
return existing || null;
|
||||
});
|
||||
const isVSCode = useMemo(() => isVSCodeRuntime(), []);
|
||||
const isDesktop = useMemo(() => isDesktopRuntime(), []);
|
||||
|
||||
const availableThemes = useMemo(() => {
|
||||
const merged: Theme[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const add = (theme: Theme) => {
|
||||
const id = theme.metadata.id;
|
||||
if (seen.has(id)) return;
|
||||
seen.add(id);
|
||||
merged.push(theme);
|
||||
};
|
||||
|
||||
if (isVSCode && vscodeTheme) {
|
||||
add(vscodeTheme);
|
||||
}
|
||||
|
||||
// Custom themes first so they can override built-ins with the same id.
|
||||
customThemes.forEach(add);
|
||||
themes.forEach(add);
|
||||
|
||||
return merged;
|
||||
}, [customThemes, isVSCode, vscodeTheme]);
|
||||
|
||||
const getThemeByIdFromAvailable = useCallback(
|
||||
(themeId: string): Theme | undefined => availableThemes.find((theme) => theme.metadata.id === themeId),
|
||||
[availableThemes],
|
||||
);
|
||||
|
||||
const ensureThemeById = useCallback(
|
||||
(themeId: string, variant: 'light' | 'dark'): Theme => {
|
||||
const theme = getThemeByIdFromAvailable(themeId);
|
||||
if (theme && theme.metadata.variant === variant) {
|
||||
return theme;
|
||||
}
|
||||
|
||||
const fallback = availableThemes.find((candidate) => candidate.metadata.variant === variant);
|
||||
return fallback ?? fallbackThemeForVariant(variant);
|
||||
},
|
||||
[availableThemes, getThemeByIdFromAvailable],
|
||||
);
|
||||
|
||||
const currentTheme = useMemo(() => {
|
||||
if (isVSCode && vscodeTheme) {
|
||||
@@ -152,12 +245,46 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
return systemPrefersDark
|
||||
? ensureThemeById(preferences.darkThemeId, 'dark')
|
||||
: ensureThemeById(preferences.lightThemeId, 'light');
|
||||
}, [isVSCode, preferences, systemPrefersDark, vscodeTheme]);
|
||||
}, [ensureThemeById, isVSCode, preferences, systemPrefersDark, vscodeTheme]);
|
||||
|
||||
const availableThemes = useMemo(
|
||||
() => (isVSCode && vscodeTheme ? [vscodeTheme, ...themes] : themes),
|
||||
[isVSCode, vscodeTheme],
|
||||
);
|
||||
const reloadCustomThemes = useCallback(async () => {
|
||||
if (typeof window === 'undefined' || isVSCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
setCustomThemesLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/config/themes', {
|
||||
method: 'GET',
|
||||
credentials: isDesktop ? 'omit' : 'include',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
// UI auth gate will handle prompting; avoid noisy retries here.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = await res.json();
|
||||
const incoming = Array.isArray(payload?.themes) ? payload.themes : [];
|
||||
const normalized = incoming.filter(isValidCustomTheme);
|
||||
setCustomThemes(normalized);
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
setCustomThemesLoading(false);
|
||||
}
|
||||
}, [isDesktop, isVSCode]);
|
||||
|
||||
useEffect(() => {
|
||||
void reloadCustomThemes();
|
||||
}, [reloadCustomThemes]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isVSCode) {
|
||||
@@ -166,11 +293,6 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
|
||||
const applyVSCodeTheme = (theme: Theme) => {
|
||||
setVSCodeTheme(theme);
|
||||
const variant: ThemeMode = theme.metadata.variant === 'dark' ? 'dark' : 'light';
|
||||
const uiStore = useUIStore.getState();
|
||||
if (uiStore.theme !== variant) {
|
||||
uiStore.setTheme(variant);
|
||||
}
|
||||
};
|
||||
|
||||
const handleThemeEvent = (event: Event) => {
|
||||
@@ -270,7 +392,17 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
'selectedThemeVariant',
|
||||
currentTheme.metadata.variant === 'light' ? 'light' : 'dark',
|
||||
);
|
||||
}, [preferences, currentTheme]);
|
||||
|
||||
// Splash screen (packages/web/index.html) runs before the theme CSS vars load.
|
||||
// Persist just enough to theme it on next boot.
|
||||
const lightTheme = ensureThemeById(preferences.lightThemeId, 'light');
|
||||
const darkTheme = ensureThemeById(preferences.darkThemeId, 'dark');
|
||||
|
||||
localStorage.setItem('splashBgLight', lightTheme.colors.surface.background);
|
||||
localStorage.setItem('splashFgLight', lightTheme.colors.surface.foreground);
|
||||
localStorage.setItem('splashBgDark', darkTheme.colors.surface.background);
|
||||
localStorage.setItem('splashFgDark', darkTheme.colors.surface.foreground);
|
||||
}, [preferences, currentTheme, ensureThemeById]);
|
||||
|
||||
useEffect(() => {
|
||||
void updateDesktopSettings({
|
||||
@@ -304,12 +436,12 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
|
||||
let nextLight = prev.lightThemeId;
|
||||
if (typeof detail.lightThemeId === 'string' && detail.lightThemeId.length > 0) {
|
||||
nextLight = validateThemeId(detail.lightThemeId, 'light');
|
||||
nextLight = detail.lightThemeId.trim();
|
||||
}
|
||||
|
||||
let nextDark = prev.darkThemeId;
|
||||
if (typeof detail.darkThemeId === 'string' && detail.darkThemeId.length > 0) {
|
||||
nextDark = validateThemeId(detail.darkThemeId, 'dark');
|
||||
nextDark = detail.darkThemeId.trim();
|
||||
}
|
||||
|
||||
const same =
|
||||
@@ -458,6 +590,8 @@ export function ThemeSystemProvider({ children, defaultThemeId }: ThemeSystemPro
|
||||
currentTheme,
|
||||
availableThemes,
|
||||
setTheme,
|
||||
customThemesLoading,
|
||||
reloadCustomThemes,
|
||||
isSystemPreference: preferences.themeMode === 'system',
|
||||
setSystemPreference: setSystemPreferenceHandler,
|
||||
themeMode: preferences.themeMode,
|
||||
|
||||
@@ -6,6 +6,8 @@ export interface ThemeContextValue {
|
||||
currentTheme: Theme;
|
||||
availableThemes: Theme[];
|
||||
setTheme: (themeId: string) => void;
|
||||
customThemesLoading: boolean;
|
||||
reloadCustomThemes: () => Promise<void>;
|
||||
isSystemPreference: boolean;
|
||||
setSystemPreference: (use: boolean) => void;
|
||||
themeMode: ThemeMode;
|
||||
|
||||
Reference in New Issue
Block a user