diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index aea75b8..45790e5 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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 (
-
-
-
-
- {children}
-
-
-
- );
-}
+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 (
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/registers/[hex]/page.tsx b/src/app/registers/[hex]/page.tsx
index 0622dcb..7e2c4c8 100644
--- a/src/app/registers/[hex]/page.tsx
+++ b/src/app/registers/[hex]/page.tsx
@@ -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();
diff --git a/src/app/registers/page.tsx b/src/app/registers/page.tsx
index 10af6ad..ecb4bc0 100644
--- a/src/app/registers/page.tsx
+++ b/src/app/registers/page.tsx
@@ -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 (
-
Spectrum Next Registers
+ NextReg Explorer
);
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
new file mode 100644
index 0000000..7c6b778
--- /dev/null
+++ b/src/components/Navbar.tsx
@@ -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 (
+
+
+ SpecNext Explorer
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/ThemeDropdown.tsx b/src/components/ThemeDropdown.tsx
new file mode 100644
index 0000000..641813b
--- /dev/null
+++ b/src/components/ThemeDropdown.tsx
@@ -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("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 (
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/scss/_bootswatch.scss b/src/scss/_bootswatch.scss
similarity index 100%
rename from src/app/scss/_bootswatch.scss
rename to src/scss/_bootswatch.scss
diff --git a/src/app/scss/_explorer.scss b/src/scss/_explorer.scss
similarity index 100%
rename from src/app/scss/_explorer.scss
rename to src/scss/_explorer.scss
diff --git a/src/app/scss/_variables.scss b/src/scss/_variables.scss
similarity index 100%
rename from src/app/scss/_variables.scss
rename to src/scss/_variables.scss
diff --git a/src/app/scss/nbn.scss b/src/scss/nbn.scss
similarity index 77%
rename from src/app/scss/nbn.scss
rename to src/scss/nbn.scss
index 4e7b6b3..c8e1474 100644
--- a/src/app/scss/nbn.scss
+++ b/src/scss/nbn.scss
@@ -6,7 +6,7 @@
@import "variables";
-@import "~bootstrap/scss/bootstrap";
+@import "../../node_modules/bootstrap/scss/bootstrap";
@import "bootswatch";
diff --git a/src/app/services/register.service.ts b/src/services/register.service.ts
similarity index 100%
rename from src/app/services/register.service.ts
rename to src/services/register.service.ts