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>
|
||||
|
||||
Reference in New Issue
Block a user