Skip to content

Commit daa9bda

Browse files
authored
Merge pull request #5 from coder/brett/theme-picker
feat: add a theme picker
2 parents bee43cf + 80ff0c1 commit daa9bda

File tree

6 files changed

+152
-12
lines changed

6 files changed

+152
-12
lines changed

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<link rel="stylesheet" href="src/index.css" />
1717
</head>
1818

19-
<body class="dark">
19+
<body>
2020
<div id="root"></div>
2121
<script type="module" src="/src/main.tsx"></script>
2222
</body>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"react-simple-code-editor": "^0.14.1",
2727
"tailwind-merge": "^3.3.0",
2828
"tailwindcss-animate": "^1.0.7",
29+
"valibot": "^1.1.0",
2930
"zustand": "^5.0.5"
3031
},
3132
"devDependencies": {

pnpm-lock.yaml

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
import { Editor } from "@/Editor";
2+
import { Preview } from "@/Preview";
3+
import { Logo } from "@/components/Logo";
14
import { ResizableHandle, ResizablePanelGroup } from "@/components/Resizable";
2-
import { Editor } from "./Editor";
3-
import { Logo } from "./components/Logo";
4-
import { Preview } from "./Preview";
55
import { useStore } from "@/store";
6-
import { useEffect } from "react";
6+
import {
7+
DropdownMenu,
8+
DropdownMenuContent,
9+
DropdownMenuItem,
10+
DropdownMenuPortal,
11+
DropdownMenuTrigger,
12+
} from "@/components/DropdownMenu";
13+
import { type FC, useEffect, useMemo } from "react";
714

815
// Glue code required to be able to run wasm compiled Go code.
916
import "@/utils/wasm_exec.js";
17+
import { useTheme } from "@/contexts/theme";
18+
import { MoonIcon, SunIcon, SunMoonIcon } from "lucide-react";
19+
import { Button } from "./components/Button";
1020

1121
type GoPreviewDef = (v: unknown) => Promise<string>;
1222

@@ -93,13 +103,11 @@ export const App = () => {
93103
>
94104
Support
95105
</a>
106+
<ThemeSelector />
96107
</div>
97108
</nav>
98109

99-
<ResizablePanelGroup
100-
aria-hidden={!$wasmState}
101-
direction={"horizontal"}
102-
>
110+
<ResizablePanelGroup aria-hidden={!$wasmState} direction={"horizontal"}>
103111
{/* EDITOR */}
104112
<Editor />
105113

@@ -111,3 +119,42 @@ export const App = () => {
111119
</main>
112120
);
113121
};
122+
123+
const ThemeSelector: FC = () => {
124+
const { theme, setTheme } = useTheme();
125+
126+
const Icon = useMemo(() => {
127+
if (theme === "system") {
128+
return SunMoonIcon;
129+
}
130+
131+
if (theme === "dark") {
132+
return MoonIcon;
133+
}
134+
135+
return SunIcon;
136+
}, [theme]);
137+
138+
return (
139+
<DropdownMenu>
140+
<DropdownMenuTrigger asChild={true}>
141+
<Button variant="subtle" size="icon-lg">
142+
<Icon height={24} width={24} />
143+
</Button>
144+
</DropdownMenuTrigger>
145+
<DropdownMenuPortal>
146+
<DropdownMenuContent align="end">
147+
<DropdownMenuItem onClick={() => setTheme("dark")}>
148+
<MoonIcon width={24} height={24} /> Dark
149+
</DropdownMenuItem>
150+
<DropdownMenuItem onClick={() => setTheme("light")}>
151+
<SunIcon width={24} height={24} /> Light
152+
</DropdownMenuItem>
153+
<DropdownMenuItem onClick={() => setTheme("system")}>
154+
<SunMoonIcon width={24} height={24} /> System
155+
</DropdownMenuItem>
156+
</DropdownMenuContent>
157+
</DropdownMenuPortal>
158+
</DropdownMenu>
159+
);
160+
};

src/contexts/theme.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import {
2+
createContext,
3+
useContext,
4+
useEffect,
5+
useState,
6+
type FC,
7+
type PropsWithChildren,
8+
} from "react";
9+
import * as v from "valibot";
10+
11+
const STORAGE_KEY = "theme";
12+
13+
const ThemeSchema = v.union([
14+
v.literal("dark"),
15+
v.literal("light"),
16+
v.literal("system"),
17+
]);
18+
type Theme = v.InferInput<typeof ThemeSchema>;
19+
20+
type ThemeContext = {
21+
theme: Theme;
22+
setTheme: (theme: Theme) => void;
23+
};
24+
25+
const ThemeContext = createContext<ThemeContext>({
26+
theme: "system",
27+
setTheme: () => null,
28+
});
29+
30+
export const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
31+
const [theme, setTheme] = useState<Theme>(() => {
32+
const parsedTheme = v.safeParse(
33+
ThemeSchema,
34+
localStorage.getItem(STORAGE_KEY),
35+
);
36+
37+
if (!parsedTheme.success) {
38+
return "system";
39+
}
40+
41+
return parsedTheme.output;
42+
});
43+
44+
useEffect(() => {
45+
const force =
46+
theme === "dark" ||
47+
(theme === "system" &&
48+
window.matchMedia("(prefers-color-scheme: dark)").matches);
49+
50+
document.documentElement.classList.toggle("dark", force);
51+
52+
if (theme === "system") {
53+
localStorage.removeItem(STORAGE_KEY);
54+
} else {
55+
localStorage.setItem(STORAGE_KEY, theme);
56+
}
57+
}, [theme]);
58+
59+
return (
60+
<ThemeContext.Provider value={{ theme, setTheme }}>
61+
{children}
62+
</ThemeContext.Provider>
63+
);
64+
};
65+
66+
export const useTheme = () => {
67+
const themeContext = useContext(ThemeContext);
68+
69+
if (!themeContext) {
70+
throw new Error("useTheme must be used within a ThemeProvider");
71+
}
72+
73+
return themeContext;
74+
};

src/main.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TooltipProvider } from "@/components/Tooltip";
44
import { StrictMode } from "react";
55
import { createRoot } from "react-dom/client";
66
import { App } from "./App.tsx";
7+
import { ThemeProvider } from "@/contexts/theme.tsx";
78

89
const root = document.getElementById("root");
910

@@ -12,9 +13,11 @@ if (!root) {
1213
} else {
1314
createRoot(root).render(
1415
<StrictMode>
15-
<TooltipProvider>
16-
<App />
17-
</TooltipProvider>
16+
<ThemeProvider>
17+
<TooltipProvider>
18+
<App />
19+
</TooltipProvider>
20+
</ThemeProvider>
1821
</StrictMode>,
1922
);
2023
}

0 commit comments

Comments
 (0)