adding appearance toggle (light/dark mode)
This commit is contained in:
+19
-2
@@ -13,6 +13,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { site } from "@/content";
|
import { site } from "@/content";
|
||||||
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
|
|
||||||
const lastUpdated = "2 May 2026";
|
const lastUpdated = "2 May 2026";
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ export default function CookiePolicyPage() {
|
|||||||
|
|
||||||
{/* Lightweight header — just a back-link */}
|
{/* Lightweight header — just a back-link */}
|
||||||
<header className="border-b border-[var(--border)]">
|
<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
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="inline-flex items-center gap-2 text-sm text-[var(--text-soft)] transition-colors hover:text-[var(--text)]"
|
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>
|
<span aria-hidden="true">←</span>
|
||||||
Back to Novarix Networks
|
Back to Novarix Networks
|
||||||
</Link>
|
</Link>
|
||||||
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -78,7 +80,7 @@ export default function CookiePolicyPage() {
|
|||||||
What we store
|
What we store
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3">
|
<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:
|
confined to your browser, and never shared with anyone:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -96,6 +98,21 @@ export default function CookiePolicyPage() {
|
|||||||
every page. Persists until you clear your browser data.
|
every page. Persists until you clear your browser data.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+33
-6
@@ -66,8 +66,7 @@
|
|||||||
`@media (prefers-color-scheme: dark)` block.
|
`@media (prefers-color-scheme: dark)` block.
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
:root {
|
:root {
|
||||||
color-scheme: light dark;
|
color-scheme: light;
|
||||||
|
|
||||||
--bg: var(--color-ink-50);
|
--bg: var(--color-ink-50);
|
||||||
--surface: oklch(1 0 0 / 0.7);
|
--surface: oklch(1 0 0 / 0.7);
|
||||||
--surface-strong: oklch(1 0 0 / 0.92);
|
--surface-strong: oklch(1 0 0 / 0.92);
|
||||||
@@ -85,8 +84,28 @@
|
|||||||
--button-hover: var(--color-ink-800);
|
--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) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root:not([data-theme]) {
|
||||||
|
color-scheme: dark;
|
||||||
--bg: var(--color-ink-950);
|
--bg: var(--color-ink-950);
|
||||||
--surface: oklch(0.13 0.028 255 / 0.55);
|
--surface: oklch(0.13 0.028 255 / 0.55);
|
||||||
--surface-strong: oklch(0.13 0.028 255 / 0.85);
|
--surface-strong: oklch(0.13 0.028 255 / 0.85);
|
||||||
@@ -218,12 +237,20 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
:root[data-theme="dark"] .brand-light {
|
||||||
.brand-light {
|
|
||||||
display: none;
|
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;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,35 @@ export default function RootLayout({
|
|||||||
}: Readonly<{ children: React.ReactNode }>) {
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en-GB" suppressHydrationWarning>
|
<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>
|
<body>
|
||||||
{children}
|
{children}
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
|
|||||||
+7
-2
@@ -27,6 +27,7 @@ import Link from "next/link";
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { site } from "@/content";
|
import { site } from "@/content";
|
||||||
import { openCookieBanner } from "@/components/CookieBanner";
|
import { openCookieBanner } from "@/components/CookieBanner";
|
||||||
|
import ThemeToggle from "@/components/ThemeToggle";
|
||||||
|
|
||||||
// Type for the mouse-pointer position used to move the background glow.
|
// Type for the mouse-pointer position used to move the background glow.
|
||||||
type PointerState = { x: number; y: number };
|
type PointerState = { x: number; y: number };
|
||||||
@@ -212,16 +213,20 @@ export default function HomePage() {
|
|||||||
Contact
|
Contact
|
||||||
<span aria-hidden="true">→</span>
|
<span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
|
<ThemeToggle />
|
||||||
</nav>
|
</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
|
<a
|
||||||
href="#contact"
|
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
|
Contact
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</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: {
|
cookies: {
|
||||||
// Short message shown in the banner. Keep it honest — this site does not
|
// 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:
|
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
|
// The two buttons. Labels are small jokes for network folk; subtitles
|
||||||
// and aria-labels make them clear to everyone else.
|
// and aria-labels make them clear to everyone else.
|
||||||
|
|||||||
Reference in New Issue
Block a user