Add a dark mode toggle
Personal, per-browser preference (localStorage), not an admin-wide Display setting like date/time format — every role can pick their own. Sets data-bs-theme on <html> to hook into Tabler's built-in dark variant, applied via an inline script in index.html before React mounts so there's no light-mode flash on load. Toggle button lives in the sidebar footer next to Sign out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82ed25b63f
commit
5c35080e22
3 files changed
+48
No files matched your search
@@ -4,6 +4,20 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Homelab Manager</title>
|
<title>Homelab Manager</title>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var stored = localStorage.getItem("hlm-theme");
|
||||||
|
var theme =
|
||||||
|
stored === "light" || stored === "dark"
|
||||||
|
? stored
|
||||||
|
: window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light";
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ import {
|
|||||||
IconStethoscope,
|
IconStethoscope,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconMenu2,
|
IconMenu2,
|
||||||
|
IconSun,
|
||||||
|
IconMoon,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import type { CurrentUser } from "../api/client";
|
import type { CurrentUser } from "../api/client";
|
||||||
|
import { getTheme, setTheme, type Theme } from "../utils/theme";
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
to: string;
|
to: string;
|
||||||
@@ -51,10 +54,17 @@ const roleRank: Record<CurrentUser["role"], number> = { viewer: 0, operator: 1,
|
|||||||
|
|
||||||
export default function AppShell({ user, children }: { user: CurrentUser; children: ReactNode }) {
|
export default function AppShell({ user, children }: { user: CurrentUser; children: ReactNode }) {
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
const [theme, setThemeState] = useState<Theme>(() => getTheme());
|
||||||
const visibleItems = NAV_ITEMS.filter(
|
const visibleItems = NAV_ITEMS.filter(
|
||||||
(item) => !item.minRole || roleRank[user.role] >= roleRank[item.minRole],
|
(item) => !item.minRole || roleRank[user.role] >= roleRank[item.minRole],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
const next: Theme = theme === "dark" ? "light" : "dark";
|
||||||
|
setTheme(next);
|
||||||
|
setThemeState(next);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<aside className="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
<aside className="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
|
||||||
@@ -87,6 +97,14 @@ export default function AppShell({ user, children }: { user: CurrentUser; childr
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="navbar-footer px-3">
|
<div className="navbar-footer px-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-secondary btn-sm w-100 mb-2 d-flex align-items-center justify-content-center gap-2"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
>
|
||||||
|
{theme === "dark" ? <IconSun size={16} /> : <IconMoon size={16} />}
|
||||||
|
{theme === "dark" ? "Light mode" : "Dark mode"}
|
||||||
|
</button>
|
||||||
<div className="text-secondary small mb-2">
|
<div className="text-secondary small mb-2">
|
||||||
Signed in as {user.name ?? user.email ?? user.sub} · {user.role}
|
Signed in as {user.name ?? user.email ?? user.sub} · {user.role}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
export type Theme = "light" | "dark";
|
||||||
|
|
||||||
|
const THEME_KEY = "hlm-theme";
|
||||||
|
|
||||||
|
export function getTheme(): Theme {
|
||||||
|
return document.documentElement.getAttribute("data-bs-theme") === "dark" ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTheme(theme: Theme): void {
|
||||||
|
document.documentElement.setAttribute("data-bs-theme", theme);
|
||||||
|
try {
|
||||||
|
localStorage.setItem(THEME_KEY, theme);
|
||||||
|
} catch {
|
||||||
|
// localStorage unavailable (private browsing etc.) — theme still applies for this page load
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user