adding appearance toggle (light/dark mode)
This commit is contained in:
+19
-2
@@ -13,6 +13,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { site } from "@/content";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
|
||||
const lastUpdated = "2 May 2026";
|
||||
|
||||
@@ -37,7 +38,7 @@ export default function CookiePolicyPage() {
|
||||
|
||||
{/* Lightweight header — just a back-link */}
|
||||
<header className="border-b border-[var(--border)]">
|
||||
<div className="mx-auto flex h-20 w-full max-w-3xl items-center px-6 sm:px-8">
|
||||
<div className="mx-auto flex h-20 w-full max-w-3xl items-center justify-between gap-4 px-6 sm:px-8">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 text-sm text-[var(--text-soft)] transition-colors hover:text-[var(--text)]"
|
||||
@@ -45,6 +46,7 @@ export default function CookiePolicyPage() {
|
||||
<span aria-hidden="true">←</span>
|
||||
Back to Novarix Networks
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -78,7 +80,7 @@ export default function CookiePolicyPage() {
|
||||
What we store
|
||||
</h2>
|
||||
<p className="mt-3">
|
||||
We use one small browser storage entry. It is first-party,
|
||||
We use two small browser storage entries. Both are first-party,
|
||||
confined to your browser, and never shared with anyone:
|
||||
</p>
|
||||
|
||||
@@ -96,6 +98,21 @@ export default function CookiePolicyPage() {
|
||||
every page. Persists until you clear your browser data.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5 backdrop-blur sm:p-6">
|
||||
<p className="font-mono text-sm font-semibold text-[var(--text)]">
|
||||
novarix-theme
|
||||
</p>
|
||||
<p className="mt-2 text-sm">
|
||||
<span className="font-semibold text-[var(--text)]">
|
||||
Preference.
|
||||
</span>{" "}
|
||||
Stored in <code>localStorage</code>. Remembers whether you
|
||||
chose light mode or dark mode so the site uses your preferred
|
||||
theme on future visits. Persists until you clear your browser
|
||||
data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
+33
-6
@@ -66,8 +66,7 @@
|
||||
`@media (prefers-color-scheme: dark)` block.
|
||||
--------------------------------------------------------------------------- */
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
color-scheme: light;
|
||||
--bg: var(--color-ink-50);
|
||||
--surface: oklch(1 0 0 / 0.7);
|
||||
--surface-strong: oklch(1 0 0 / 0.92);
|
||||
@@ -85,8 +84,28 @@
|
||||
--button-hover: var(--color-ink-800);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bg: var(--color-ink-950);
|
||||
--surface: oklch(0.13 0.028 255 / 0.55);
|
||||
--surface-strong: oklch(0.13 0.028 255 / 0.85);
|
||||
--text: var(--color-ink-100);
|
||||
--text-soft: var(--color-ink-400);
|
||||
--border: oklch(1 0 0 / 0.08);
|
||||
--border-strong: oklch(1 0 0 / 0.16);
|
||||
--accent: var(--color-brand-400);
|
||||
--accent-soft: oklch(0.7 0.14 240 / 0.18);
|
||||
--ring: oklch(0.7 0.14 240 / 0.5);
|
||||
--grid: oklch(0.84 0.012 250 / 0.06);
|
||||
|
||||
--button-bg: var(--color-ink-100);
|
||||
--button-fg: var(--color-ink-950);
|
||||
--button-hover: var(--color-ink-50);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
:root:not([data-theme]) {
|
||||
color-scheme: dark;
|
||||
--bg: var(--color-ink-950);
|
||||
--surface: oklch(0.13 0.028 255 / 0.55);
|
||||
--surface-strong: oklch(0.13 0.028 255 / 0.85);
|
||||
@@ -218,12 +237,20 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.brand-light {
|
||||
:root[data-theme="dark"] .brand-light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.brand-dark {
|
||||
:root[data-theme="dark"] .brand-dark {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .brand-light {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:root:not([data-theme]) .brand-dark {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,35 @@ export default function RootLayout({
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en-GB" suppressHydrationWarning>
|
||||
<head>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function () {
|
||||
var key = "novarix-theme";
|
||||
var root = document.documentElement;
|
||||
var theme = "light";
|
||||
|
||||
try {
|
||||
var saved = window.localStorage.getItem(key);
|
||||
if (saved === "light" || saved === "dark") {
|
||||
theme = saved;
|
||||
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
theme = "dark";
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
theme = "dark";
|
||||
}
|
||||
}
|
||||
|
||||
root.dataset.theme = theme;
|
||||
root.style.colorScheme = theme;
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
{children}
|
||||
<CookieBanner />
|
||||
|
||||
+7
-2
@@ -27,6 +27,7 @@ import Link from "next/link";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { site } from "@/content";
|
||||
import { openCookieBanner } from "@/components/CookieBanner";
|
||||
import ThemeToggle from "@/components/ThemeToggle";
|
||||
|
||||
// Type for the mouse-pointer position used to move the background glow.
|
||||
type PointerState = { x: number; y: number };
|
||||
@@ -212,16 +213,20 @@ export default function HomePage() {
|
||||
Contact
|
||||
<span aria-hidden="true">→</span>
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
|
||||
{/* Mobile-only Contact pill — replaces the full nav on small screens */}
|
||||
{/* Mobile controls — simplified for smaller screens */}
|
||||
<div className="flex items-center gap-2 sm:hidden">
|
||||
<ThemeToggle className="text-xs" />
|
||||
<a
|
||||
href="#contact"
|
||||
className="inline-flex items-center rounded-full border border-[var(--border-strong)] px-3 py-1.5 text-xs text-[var(--text)] transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)] sm:hidden"
|
||||
className="inline-flex items-center rounded-full border border-[var(--border-strong)] px-3 py-1.5 text-xs text-[var(--text)] transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)]"
|
||||
>
|
||||
Contact
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ===================================================================
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
const THEME_KEY = "novarix-theme";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
}
|
||||
|
||||
export default function ThemeToggle({
|
||||
className = "",
|
||||
}: {
|
||||
className?: string;
|
||||
}) {
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const current =
|
||||
document.documentElement.dataset.theme === "dark" ? "dark" : "light";
|
||||
setTheme(current);
|
||||
}, []);
|
||||
|
||||
function toggleTheme() {
|
||||
const nextTheme: Theme = theme === "dark" ? "light" : "dark";
|
||||
setTheme(nextTheme);
|
||||
applyTheme(nextTheme);
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(THEME_KEY, nextTheme);
|
||||
} catch {
|
||||
/* storage unavailable — silently ignore */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleTheme}
|
||||
aria-label={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
||||
title={`Switch to ${theme === "dark" ? "light" : "dark"} mode`}
|
||||
className={`inline-flex items-center rounded-full border border-[var(--border-strong)] px-3 py-1.5 text-sm text-[var(--text)] transition-colors hover:border-[var(--accent)] hover:text-[var(--accent)] ${className}`.trim()}
|
||||
>
|
||||
{theme === "dark" ? "Light mode" : "Dark mode"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
+3
-2
@@ -153,9 +153,10 @@ export const site = {
|
||||
// ---------------------------------------------------------------------------
|
||||
cookies: {
|
||||
// Short message shown in the banner. Keep it honest — this site does not
|
||||
// currently use any tracking cookies, only one preference for consent.
|
||||
// currently use any tracking cookies, only small preferences for consent
|
||||
// and theme choice.
|
||||
message:
|
||||
"Heads up — this site stores one tiny browser preference to remember your cookie choice. We don’t run analytics, advertising, or third-party tracking.",
|
||||
"Heads up — this site stores small browser preferences to remember your cookie choice and theme selection. We don’t run analytics, advertising, or third-party tracking.",
|
||||
|
||||
// The two buttons. Labels are small jokes for network folk; subtitles
|
||||
// and aria-labels make them clear to everyone else.
|
||||
|
||||
Reference in New Issue
Block a user