feat: add dark/light theme toggle with Layonara color palette
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { Outlet, Link, useLocation } from "react-router-dom";
|
||||
import { Terminal } from "../components/terminal/Terminal";
|
||||
import { useWebSocket } from "../hooks/useWebSocket";
|
||||
import { useTheme } from "../hooks/useTheme";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ path: "/", label: "Home" },
|
||||
@@ -17,6 +18,7 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
|
||||
const [upstreamBehind, setUpstreamBehind] = useState(0);
|
||||
const location = useLocation();
|
||||
const { subscribe } = useWebSocket();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
return subscribe("git:upstream-update", (event) => {
|
||||
@@ -48,7 +50,7 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
|
||||
>
|
||||
Layonara Forge
|
||||
</h1>
|
||||
<nav className="flex items-center gap-1">
|
||||
<nav className="flex flex-1 items-center gap-1">
|
||||
{NAV_ITEMS.map((item) => {
|
||||
const isActive =
|
||||
item.path === "/" ? location.pathname === "/" : location.pathname.startsWith(item.path);
|
||||
@@ -72,6 +74,14 @@ export function IDELayout({ sidebar }: { sidebar?: React.ReactNode }) {
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
|
||||
Reference in New Issue
Block a user