Skip to content

Commit 43f8300

Browse files
committed
Refactor theme management to use useTheme hook and ThemeContext
- Create ThemeContext with useTheme hook for cleaner theme access - Remove duplicate theme logic from App.tsx - Simplify Storybook decorator to just apply data-theme attribute - Components can now use useTheme() instead of reading document.documentElement - Centralize all theme logic in one place
1 parent 438b2d2 commit 43f8300

File tree

4 files changed

+55
-24
lines changed

4 files changed

+55
-24
lines changed

.storybook/preview.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { Preview } from "@storybook/react-vite";
2-
import { useEffect } from "react";
32
import "../src/browser/styles/globals.css";
43

54
const preview: Preview = {
@@ -22,14 +21,6 @@ const preview: Preview = {
2221
(Story, context) => {
2322
const theme = context.globals.theme || "dark";
2423

25-
useEffect(() => {
26-
// Apply theme to document root
27-
document.documentElement.dataset.theme = theme;
28-
29-
// Also apply to body to ensure it persists
30-
document.body.dataset.theme = theme;
31-
}, [theme]);
32-
3324
return (
3425
<div data-theme={theme} style={{ minHeight: "100vh", backgroundColor: "var(--color-background)", color: "var(--color-foreground)" }}>
3526
<Story />

src/browser/App.tsx

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ import { CommandRegistryProvider, useCommandRegistry } from "./contexts/CommandR
2222
import type { CommandAction } from "./contexts/CommandRegistryContext";
2323
import { ModeProvider } from "./contexts/ModeContext";
2424
import { ThinkingProvider } from "./contexts/ThinkingContext";
25+
import { ThemeProvider } from "./contexts/ThemeContext";
2526
import { CommandPalette } from "./components/CommandPalette";
2627
import { buildCoreSources, type BuildSourcesParams } from "./utils/commands/sources";
2728

2829
import type { ThinkingLevel } from "@/common/types/thinking";
2930
import { CUSTOM_EVENTS } from "@/common/constants/events";
3031
import { isWorkspaceForkSwitchEvent } from "./utils/workspaceFork";
31-
import { getThinkingLevelKey, THEME_KEY } from "@/common/constants/storage";
32+
import { getThinkingLevelKey } from "@/common/constants/storage";
3233
import type { BranchListResult } from "@/common/types/ipc";
3334
import { useTelemetry } from "./hooks/useTelemetry";
3435
import { useStartWorkspaceCreation, getFirstProjectPath } from "./hooks/useStartWorkspaceCreation";
36+
import { useTheme } from "./contexts/ThemeContext";
3537

3638
const THINKING_LEVELS: ThinkingLevel[] = ["off", "low", "medium", "high"];
3739

@@ -62,12 +64,7 @@ function AppInner() {
6264
const [sidebarCollapsed, setSidebarCollapsed] = usePersistedState("sidebarCollapsed", isMobile);
6365

6466
// Theme state (global, not workspace-specific)
65-
const [theme, setTheme] = usePersistedState<"light" | "dark">(THEME_KEY, "dark");
66-
67-
// Apply theme to document root
68-
useEffect(() => {
69-
document.documentElement.dataset.theme = theme;
70-
}, [theme]);
67+
const { toggleTheme } = useTheme();
7168

7269
const defaultProjectPath = getFirstProjectPath(projects);
7370
const creationChatInputRef = useRef<ChatInputAPI | null>(null);
@@ -390,10 +387,6 @@ function AppInner() {
390387
setSidebarCollapsed((prev) => !prev);
391388
}, [setSidebarCollapsed]);
392389

393-
const toggleThemeFromPalette = useCallback(() => {
394-
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
395-
}, [setTheme]);
396-
397390
const navigateWorkspaceFromPalette = useCallback(
398391
(dir: "next" | "prev") => {
399392
handleNavigateWorkspace(dir);
@@ -415,7 +408,7 @@ function AppInner() {
415408
onAddProject: addProjectFromPalette,
416409
onRemoveProject: removeProjectFromPalette,
417410
onToggleSidebar: toggleSidebarFromPalette,
418-
onToggleTheme: toggleThemeFromPalette,
411+
onToggleTheme: toggleTheme,
419412
onNavigateWorkspace: navigateWorkspaceFromPalette,
420413
onOpenWorkspaceInTerminal: openWorkspaceInTerminal,
421414
};
@@ -632,9 +625,15 @@ function AppInner() {
632625

633626
function App() {
634627
return (
635-
<CommandRegistryProvider>
636-
<AppInner />
637-
</CommandRegistryProvider>
628+
<ThemeProvider>
629+
<CommandRegistryProvider>
630+
<ModeProvider>
631+
<ThinkingProvider>
632+
<AppInner />
633+
</ThinkingProvider>
634+
</ModeProvider>
635+
</CommandRegistryProvider>
636+
</ThemeProvider>
638637
);
639638
}
640639

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { createContext, useContext, useEffect, type ReactNode } from "react";
2+
import { usePersistedState } from "../hooks/usePersistedState";
3+
import { THEME_KEY } from "@/common/constants/storage";
4+
5+
type Theme = "light" | "dark";
6+
7+
interface ThemeContextValue {
8+
theme: Theme;
9+
setTheme: (theme: Theme) => void;
10+
toggleTheme: () => void;
11+
}
12+
13+
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
14+
15+
export function ThemeProvider(props: { children: ReactNode }) {
16+
const [theme, setTheme] = usePersistedState<Theme>(THEME_KEY, "dark");
17+
18+
// Apply theme to document root
19+
useEffect(() => {
20+
document.documentElement.dataset.theme = theme;
21+
}, [theme]);
22+
23+
const toggleTheme = () => {
24+
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
25+
};
26+
27+
return (
28+
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
29+
{props.children}
30+
</ThemeContext.Provider>
31+
);
32+
}
33+
34+
export function useTheme(): ThemeContextValue {
35+
const context = useContext(ThemeContext);
36+
if (!context) {
37+
throw new Error("useTheme must be used within ThemeProvider");
38+
}
39+
return context;
40+
}

src/browser/utils/highlighting/shiki-shared.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
/**
77
* Get the appropriate Shiki theme based on the current theme
8+
* Falls back to reading from document if called outside React context
89
*/
910
export function getShikiTheme(): "min-dark" | "min-light" {
1011
if (typeof document === "undefined") return "min-dark";

0 commit comments

Comments
 (0)