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 { 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user