feat: add dark/light theme toggle with Layonara color palette

This commit is contained in:
plenarius
2026-04-20 22:08:41 -04:00
parent 8a3cb1b0a3
commit 1255cee8e6
2 changed files with 50 additions and 1 deletions
+39
View File
@@ -0,0 +1,39 @@
import { useState, useEffect, useCallback } from "react";
type Theme = "dark" | "light";
const STORAGE_KEY = "forge-theme";
function getInitialTheme(): Theme {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored === "light" || stored === "dark") return stored;
} catch {
// localStorage unavailable
}
return "dark";
}
export function useTheme() {
const [theme, setTheme] = useState<Theme>(getInitialTheme);
useEffect(() => {
const root = document.documentElement;
if (theme === "light") {
root.classList.add("light");
} else {
root.classList.remove("light");
}
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch {
// localStorage unavailable
}
}, [theme]);
const toggleTheme = useCallback(() => {
setTheme((t) => (t === "dark" ? "light" : "dark"));
}, []);
return { theme, toggleTheme } as const;
}
+11 -1
View File
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
import { Outlet, Link, useLocation } from "react-router-dom"; import { Outlet, Link, useLocation } from "react-router-dom";
import { Terminal } from "../components/terminal/Terminal"; import { Terminal } from "../components/terminal/Terminal";
import { useWebSocket } from "../hooks/useWebSocket"; import { useWebSocket } from "../hooks/useWebSocket";
import { useTheme } from "../hooks/useTheme";
const NAV_ITEMS = [ const NAV_ITEMS = [
{ path: "/", label: "Home" }, { path: "/", label: "Home" },
@@ -17,6 +18,7 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
const [upstreamBehind, setUpstreamBehind] = useState(0); const [upstreamBehind, setUpstreamBehind] = useState(0);
const location = useLocation(); const location = useLocation();
const { subscribe } = useWebSocket(); const { subscribe } = useWebSocket();
const { theme, toggleTheme } = useTheme();
useEffect(() => { useEffect(() => {
return subscribe("git:upstream-update", (event) => { return subscribe("git:upstream-update", (event) => {
@@ -48,7 +50,7 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
> >
Layonara Forge Layonara Forge
</h1> </h1>
<nav className="flex items-center gap-1"> <nav className="flex flex-1 items-center gap-1">
{NAV_ITEMS.map((item) => { {NAV_ITEMS.map((item) => {
const isActive = const isActive =
item.path === "/" ? location.pathname === "/" : location.pathname.startsWith(item.path); item.path === "/" ? location.pathname === "/" : location.pathname.startsWith(item.path);
@@ -72,6 +74,14 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
); );
})} })}
</nav> </nav>
<button
onClick={toggleTheme}
className="rounded px-2 py-1 text-sm transition-colors hover:bg-white/10"
style={{ color: "var(--forge-text-secondary)" }}
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
>
{theme === "dark" ? "\u2600\uFE0F" : "\uD83C\uDF19"}
</button>
</header> </header>
<div className="flex flex-1 flex-col overflow-hidden"> <div className="flex flex-1 flex-col overflow-hidden">