From fcbe34b027a62da7c352c19d0e574b2d88bce2b1 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Sun, 12 Jul 2026 14:09:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(web):=20app=20shell=20=E2=80=94=20masthead?= =?UTF-8?q?,=20contents=20nav,=20theme=20toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 and persists to localStorage. Pages no longer repeat the ad-hoc nav line. Co-Authored-By: Claude Opus 4.8 (1M context) --- web/src/app/_ui/contents-nav.tsx | 28 ++++++++++++++++++++++++++++ web/src/app/_ui/masthead.tsx | 21 +++++++++++++++++++++ web/src/app/_ui/theme-toggle.tsx | 29 +++++++++++++++++++++++++++++ web/src/app/layout.tsx | 16 ++++++++++++++-- 4 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 web/src/app/_ui/contents-nav.tsx create mode 100644 web/src/app/_ui/masthead.tsx create mode 100644 web/src/app/_ui/theme-toggle.tsx diff --git a/web/src/app/_ui/contents-nav.tsx b/web/src/app/_ui/contents-nav.tsx new file mode 100644 index 0000000..ebc6ab5 --- /dev/null +++ b/web/src/app/_ui/contents-nav.tsx @@ -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 ( + + ); +} diff --git a/web/src/app/_ui/masthead.tsx b/web/src/app/_ui/masthead.tsx new file mode 100644 index 0000000..6f5cea8 --- /dev/null +++ b/web/src/app/_ui/masthead.tsx @@ -0,0 +1,21 @@ +import { ThemeToggle } from "./theme-toggle"; + +export function Masthead() { + return ( +
+
+

+ Lyra +

+
A record label for an audience of one
+
+
+
+ + Worker ยท listening +
+ +
+
+ ); +} diff --git a/web/src/app/_ui/theme-toggle.tsx b/web/src/app/_ui/theme-toggle.tsx new file mode 100644 index 0000000..cb3ca34 --- /dev/null +++ b/web/src/app/_ui/theme-toggle.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useEffect, useState } from "react"; + +// Stamps data-theme on (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 ( + + ); +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 7d75d54..7e0113d 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -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 default function RootLayout({ children }: { children: React.ReactNode }) { return ( - - {children} + + +
+ + + {children} +
+ ); }