feat(web): app shell — masthead, contents nav, theme toggle
Root layout now wraps every page in the editorial column with a masthead (wordmark + tagline + live worker dot) and a contents-bar nav with active state via usePathname. Theme toggle stamps data-theme on <html> and persists to localStorage. Pages no longer repeat the ad-hoc nav line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
const LINKS: { href: string; label: string }[] = [
|
||||||
|
{ href: "/", label: "The Floor" },
|
||||||
|
{ href: "/artists", label: "Artists" },
|
||||||
|
{ href: "/discover", label: "Discover" },
|
||||||
|
{ href: "/wanted", label: "Wanted" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ContentsNav() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const active = (href: string) => (href === "/" ? pathname === "/" : pathname.startsWith(href));
|
||||||
|
return (
|
||||||
|
<nav className="contents">
|
||||||
|
{LINKS.map((l) => (
|
||||||
|
<a key={l.href} href={l.href} className={active(l.href) ? "here" : ""}>
|
||||||
|
{l.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<span className="sep" />
|
||||||
|
<a href="/settings" className={active("/settings") ? "here" : ""}>
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { ThemeToggle } from "./theme-toggle";
|
||||||
|
|
||||||
|
export function Masthead() {
|
||||||
|
return (
|
||||||
|
<header className="masthead">
|
||||||
|
<div className="wordmark">
|
||||||
|
<h1>
|
||||||
|
<a href="/">Lyra</a>
|
||||||
|
</h1>
|
||||||
|
<div className="tagline">A record label for an audience of one</div>
|
||||||
|
</div>
|
||||||
|
<div className="press-status">
|
||||||
|
<div>
|
||||||
|
<span className="live-dot" />
|
||||||
|
Worker · listening
|
||||||
|
</div>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Stamps data-theme on <html> (which wins over the OS preference in globals.css)
|
||||||
|
// and remembers the choice. Starts from the saved value or the OS preference.
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const [theme, setTheme] = useState<"light" | "dark">("light");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const saved = localStorage.getItem("lyra-theme") as "light" | "dark" | null;
|
||||||
|
const initial = saved ?? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
|
||||||
|
setTheme(initial);
|
||||||
|
document.documentElement.setAttribute("data-theme", initial);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
const next = theme === "dark" ? "light" : "dark";
|
||||||
|
setTheme(next);
|
||||||
|
document.documentElement.setAttribute("data-theme", next);
|
||||||
|
localStorage.setItem("lyra-theme", next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button type="button" className="btn" aria-label="Toggle theme" onClick={toggle}>
|
||||||
|
{theme === "dark" ? "Day" : "Night"}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
+14
-2
@@ -1,9 +1,21 @@
|
|||||||
|
import "./globals.css";
|
||||||
|
import "./design.css";
|
||||||
|
import { fraunces } from "./fonts";
|
||||||
|
import { Masthead } from "./_ui/masthead";
|
||||||
|
import { ContentsNav } from "./_ui/contents-nav";
|
||||||
|
|
||||||
export const metadata = { title: "Lyra" };
|
export const metadata = { title: "Lyra" };
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className={fraunces.variable} suppressHydrationWarning>
|
||||||
<body>{children}</body>
|
<body>
|
||||||
|
<div className="wrap">
|
||||||
|
<Masthead />
|
||||||
|
<ContentsNav />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user