Attempt 1 to add a darkmode

This commit is contained in:
2025-10-17 12:35:48 +01:00
parent 8df71a6457
commit 59d16ebde1
10 changed files with 200 additions and 43 deletions

View File

@@ -1,44 +1,38 @@
import type { Metadata } from "next";
import Link from 'next/link';
import "./scss/nbn.scss";
import "@/scss/nbn.scss";
import NavbarClient from "@/components/Navbar";
export const metadata: Metadata = {
title: "Spectrum Next Registers",
description: "A platform for exploring the Spectrum Next registers",
robots: { index: true, follow: true },
formatDetection: { email: false, address: false, telephone: false },
title: "Spectrum Next Explorer",
description: "A platform for exploring the Spectrum Next hardware",
robots: { index: true, follow: true },
formatDetection: { email: false, address: false, telephone: false },
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<nav className="navbar navbar-expand-lg navbar-dark bg-dark sticky-top">
<div className="container-fluid">
<Link className="navbar-brand" href="/">Next Explorer</Link>
<button className="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span className="navbar-toggler-icon"></span>
</button>
<div className="collapse navbar-collapse" id="navbarSupportedContent">
<ul className="navbar-nav me-auto mb-2 mb-lg-0">
<li className="nav-item">
<Link className="nav-link" href="/">Home</Link>
</li>
<li className="nav-item">
<Link className="nav-link" href="/registers">Registers</Link>
</li>
</ul>
</div>
</div>
</nav>
<div className="container-fluid py-3">
{children}
</div>
</body>
</html>
);
}
const noFlashThemeScript = `
(function() {
try {
var cookieMatch = document.cookie.match(/(?:^|; )theme=([^;]+)/);
var stored = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var effective = (stored === 'light' || stored === 'dark') ? stored : (prefersDark ? 'dark' : 'light');
var el = document.documentElement;
if (el.getAttribute('data-bs-theme') !== effective) {
el.setAttribute('data-bs-theme', effective);
}
} catch(_) {}
})();`;
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: noFlashThemeScript }} />
</head>
<body>
<NavbarClient />
<div className="container-fluid py-3">{children}</div>
</body>
</html>
);
}

View File

@@ -3,7 +3,7 @@ import Link from 'next/link';
import { Register } from '@/utils/register_parser';
import RegisterDetail from '@/app/registers/RegisterDetail';
import {Container, Row} from "react-bootstrap";
import { getRegisters } from '@/app/services/register.service';
import { getRegisters } from '@/services/register.service';
export default async function RegisterDetailPage({ params }: { params: { hex: string } }) {
const registers = await getRegisters();

View File

@@ -1,12 +1,12 @@
import RegisterBrowser from '@/app/registers/RegisterBrowser';
import { getRegisters } from '@/app/services/register.service';
import { getRegisters } from '@/services/register.service';
export default async function RegistersPage() {
const registers = await getRegisters();
return (
<div className="container-fluid py-4">
<h1 className="mb-4">Spectrum Next Registers</h1>
<h1 className="mb-4">NextReg Explorer</h1>
<RegisterBrowser registers={registers} />
</div>
);

25
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,25 @@
"use client";
import Link from "next/link";
import * as Icon from "react-bootstrap-icons";
import { Navbar, Nav, Container, Dropdown } from "react-bootstrap";
import ThemeDropdown from "@/components/ThemeDropdown";
export default function NavbarClient() {
return (
<Navbar expand="lg" bg="primary" data-bs-theme="dark" sticky="top" className="navbar">
<Container fluid>
<Link className="navbar-brand" href="/">SpecNext Explorer</Link>
<Navbar.Toggle aria-controls="navbarSupportedContent" />
<Navbar.Collapse id="navbarSupportedContent">
<Nav className="me-auto mb-2 mb-lg-0">
<Link className="nav-link" href="/">Home</Link>
<Link className="nav-link" href="/registers">Registers</Link>
</Nav>
<ThemeDropdown />
</Navbar.Collapse>
</Container>
</Navbar>
);
}

View File

@@ -0,0 +1,138 @@
"use client";
import { useEffect, useMemo, useState, useCallback } from "react";
import * as Icon from "react-bootstrap-icons";
import { Nav, Dropdown } from "react-bootstrap";
type Theme = "light" | "dark" | "auto";
const COOKIE = "theme";
function getCookie(name: string): string | null {
if (typeof document === "undefined") return null;
const m = document.cookie.match(new RegExp("(^|; )" + name + "=([^;]+)"));
return m ? decodeURIComponent(m[2]) : null;
}
function setCookie(name: string, value: string) {
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=31536000; SameSite=Lax`;
}
// Use a single function to read current system preference (works on iOS)
const prefersDark = () =>
typeof window !== "undefined" &&
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
export default function ThemeDropdown() {
const [mounted, setMounted] = useState(false);
const [theme, setThemeState] = useState<Theme>("auto");
const applyTheme = useCallback((t: Theme) => {
const effective = t === "auto" ? (prefersDark() ? "dark" : "light") : t;
document.documentElement.setAttribute("data-bs-theme", effective);
}, []);
// Initial mount: read cookie and APPLY immediately (important for iOS)
useEffect(() => {
setMounted(true);
const v = getCookie(COOKIE);
const initial: Theme = v === "light" || v === "dark" || v === "auto" ? (v as Theme) : "auto";
setThemeState(initial);
applyTheme(initial); // ensure render matches auto right away
}, [applyTheme]);
// Follow system changes while in auto; include iOS visibility/page-show events
useEffect(() => {
if (!mounted || theme !== "auto") return;
const mql = window.matchMedia("(prefers-color-scheme: dark)");
const onChange = () => applyTheme("auto");
// Safari <14 uses addListener/removeListener
if (typeof mql.addEventListener === "function") {
mql.addEventListener("change", onChange);
} else if (typeof mql.addListener === "function") {
mql.addListener(onChange);
}
const onVisibility = () => applyTheme("auto");
const onPageShow = () => applyTheme("auto");
document.addEventListener("visibilitychange", onVisibility);
window.addEventListener("pageshow", onPageShow);
return () => {
if (typeof mql.removeEventListener === "function") {
mql.removeEventListener("change", onChange);
} else if (typeof mql.removeListener === "function") {
mql.removeListener(onChange);
}
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("pageshow", onPageShow);
};
}, [mounted, theme, applyTheme]);
const handleSetTheme = (t: Theme) => {
setCookie(COOKIE, t);
setThemeState(t);
applyTheme(t);
};
const isActive = (t: Theme) => theme === t;
const ToggleIcon = !mounted
? Icon.CircleHalf
: theme === "dark"
? Icon.MoonStarsFill
: theme === "light"
? Icon.SunFill
: Icon.CircleHalf;
return (
<Nav className="ms-md-auto">
<Dropdown as={Nav.Item} align="end">
<Dropdown.Toggle as={Nav.Link} id="bd-theme" className="px-0 px-lg-2 py-2 d-flex align-items-center">
<ToggleIcon />
<span className="d-lg-none ms-2" id="bd-theme-text">Toggle theme</span>
</Dropdown.Toggle>
<Dropdown.Menu aria-labelledby="bd-theme-text">
<Dropdown.Item
as="button"
className="d-flex align-items-center"
aria-pressed={isActive("light")}
active={isActive("light")}
onClick={() => handleSetTheme("light")}
>
<Icon.SunFill />
<span className="ms-2">Light</span>
{isActive("light") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
</Dropdown.Item>
<Dropdown.Item
as="button"
className="d-flex align-items-center"
aria-pressed={isActive("dark")}
active={isActive("dark")}
onClick={() => handleSetTheme("dark")}
>
<Icon.MoonStarsFill />
<span className="ms-2">Dark</span>
{isActive("dark") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
</Dropdown.Item>
<Dropdown.Item
as="button"
className="d-flex align-items-center"
aria-pressed={isActive("auto")}
active={isActive("auto")}
onClick={() => handleSetTheme("auto")}
>
<Icon.CircleHalf />
<span className="ms-2">Auto</span>
{isActive("auto") && <Icon.Check2 className="ms-auto" aria-hidden="true" />}
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Nav>
);
}

View File

@@ -6,7 +6,7 @@
@import "variables";
@import "~bootstrap/scss/bootstrap";
@import "../../node_modules/bootstrap/scss/bootstrap";
@import "bootswatch";