diff --git a/index.html b/index.html index 34f464f..66cec23 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - +
diff --git a/package.json b/package.json index 6dc21fe..98cf20a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-simple-code-editor": "^0.14.1", "tailwind-merge": "^3.3.0", "tailwindcss-animate": "^1.0.7", + "valibot": "^1.1.0", "zustand": "^5.0.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ff4847..eb8c01f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.17) + valibot: + specifier: ^1.1.0 + version: 1.1.0(typescript@5.8.3) zustand: specifier: ^5.0.5 version: 5.0.5(@types/react@19.1.4)(react@19.1.0) @@ -1831,6 +1834,14 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valibot@1.1.0: + resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite@6.3.5: resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3533,6 +3544,10 @@ snapshots: util-deprecate@1.0.2: {} + valibot@1.1.0(typescript@5.8.3): + optionalDependencies: + typescript: 5.8.3 + vite@6.3.5(@types/node@22.15.21)(jiti@1.21.7)(yaml@2.8.0): dependencies: esbuild: 0.25.4 diff --git a/src/App.tsx b/src/App.tsx index 17b7c12..b3b8a24 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,22 @@ +import { Editor } from "@/Editor"; +import { Preview } from "@/Preview"; +import { Logo } from "@/components/Logo"; import { ResizableHandle, ResizablePanelGroup } from "@/components/Resizable"; -import { Editor } from "./Editor"; -import { Logo } from "./components/Logo"; -import { Preview } from "./Preview"; import { useStore } from "@/store"; -import { useEffect } from "react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuPortal, + DropdownMenuTrigger, +} from "@/components/DropdownMenu"; +import { type FC, useEffect, useMemo } from "react"; // Glue code required to be able to run wasm compiled Go code. import "@/utils/wasm_exec.js"; +import { useTheme } from "@/contexts/theme"; +import { MoonIcon, SunIcon, SunMoonIcon } from "lucide-react"; +import { Button } from "./components/Button"; type GoPreviewDef = (v: unknown) => Promise; @@ -93,13 +103,11 @@ export const App = () => { > Support + - + {/* EDITOR */} @@ -111,3 +119,42 @@ export const App = () => { ); }; + +const ThemeSelector: FC = () => { + const { theme, setTheme } = useTheme(); + + const Icon = useMemo(() => { + if (theme === "system") { + return SunMoonIcon; + } + + if (theme === "dark") { + return MoonIcon; + } + + return SunIcon; + }, [theme]); + + return ( + + + + + + + setTheme("dark")}> + Dark + + setTheme("light")}> + Light + + setTheme("system")}> + System + + + + + ); +}; diff --git a/src/contexts/theme.tsx b/src/contexts/theme.tsx new file mode 100644 index 0000000..7fcc674 --- /dev/null +++ b/src/contexts/theme.tsx @@ -0,0 +1,74 @@ +import { + createContext, + useContext, + useEffect, + useState, + type FC, + type PropsWithChildren, +} from "react"; +import * as v from "valibot"; + +const STORAGE_KEY = "theme"; + +const ThemeSchema = v.union([ + v.literal("dark"), + v.literal("light"), + v.literal("system"), +]); +type Theme = v.InferInput; + +type ThemeContext = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const ThemeContext = createContext({ + theme: "system", + setTheme: () => null, +}); + +export const ThemeProvider: FC = ({ children }) => { + const [theme, setTheme] = useState(() => { + const parsedTheme = v.safeParse( + ThemeSchema, + localStorage.getItem(STORAGE_KEY), + ); + + if (!parsedTheme.success) { + return "system"; + } + + return parsedTheme.output; + }); + + useEffect(() => { + const force = + theme === "dark" || + (theme === "system" && + window.matchMedia("(prefers-color-scheme: dark)").matches); + + document.documentElement.classList.toggle("dark", force); + + if (theme === "system") { + localStorage.removeItem(STORAGE_KEY); + } else { + localStorage.setItem(STORAGE_KEY, theme); + } + }, [theme]); + + return ( + + {children} + + ); +}; + +export const useTheme = () => { + const themeContext = useContext(ThemeContext); + + if (!themeContext) { + throw new Error("useTheme must be used within a ThemeProvider"); + } + + return themeContext; +}; diff --git a/src/main.tsx b/src/main.tsx index c466528..79df65c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,6 +4,7 @@ import { TooltipProvider } from "@/components/Tooltip"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { App } from "./App.tsx"; +import { ThemeProvider } from "@/contexts/theme.tsx"; const root = document.getElementById("root"); @@ -12,9 +13,11 @@ if (!root) { } else { createRoot(root).render( - - - + + + + + , ); }