added dark/light mode and control

This commit is contained in:
Hunter W.
2026-03-27 13:01:56 -04:00
parent 8bc880fe10
commit 84a6fe4376
7 changed files with 212 additions and 43 deletions

View File

@@ -1,52 +1,52 @@
:root {
/* Background */
--bg: #101114;
--bg-raised: #141518;
--surface: #1a1b1f;
--surface-hover: #212229;
--surface-active: #282a33;
--bg: #ffffff;
--bg-raised: #f8f8fa;
--surface: #f1f2f4;
--surface-hover: #e8e9ed;
--surface-active: #dfe0e5;
/* Border */
--border: #2c2d34;
--border-strong: #3d3f48;
--border-subtle: #232429;
--border: #d4d5db;
--border-strong: #b8bac2;
--border-subtle: #e4e5ea;
/* Foreground / text */
--fg: #e4e5e9;
--fg-secondary: #a0a3ad;
--muted: #5f6270;
--faint: #43454f;
--fg: #131419;
--fg-secondary: #4e5264;
--muted: #7e8290;
--faint: #b0b3bd;
/* Accent */
--accent: #8a9dff;
--accent-hover: #9dacff;
--accent-strong: #7088ff;
--accent-bg: rgba(138, 157, 255, 0.08);
--accent-bg-hover: rgba(138, 157, 255, 0.14);
--accent: #5468e0;
--accent-hover: #4758cc;
--accent-strong: #3d4cb8;
--accent-bg: rgba(84, 104, 224, 0.07);
--accent-bg-hover: rgba(84, 104, 224, 0.13);
/* Semantic */
--success: #5ec98f;
--success-bg: rgba(94, 201, 143, 0.08);
--warning: #e0a855;
--warning-bg: rgba(224, 168, 85, 0.08);
--error: #e06565;
--error-bg: rgba(224, 101, 101, 0.08);
--info: #60a5d6;
--info-bg: rgba(96, 165, 214, 0.08);
--success: #1a8f54;
--success-bg: rgba(26, 143, 84, 0.07);
--warning: #b07818;
--warning-bg: rgba(176, 120, 24, 0.07);
--error: #c93c3c;
--error-bg: rgba(201, 60, 60, 0.07);
--info: #2e7db5;
--info-bg: rgba(46, 125, 181, 0.07);
/* Overlay / depth */
--overlay: rgba(0, 0, 0, 0.55);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.30);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.35);
--overlay: rgba(0, 0, 0, 0.2);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.10);
/* Radii */
--radius-sm: 4px;
--radius-md: 6px;
--radius-lg: 10px;
--radius-xl: 16px;
/* Spacing */
--space-1: 4px;
--space-2: 8px;
@@ -58,7 +58,7 @@
--space-10: 40px;
--space-12: 48px;
--space-16: 64px;
/* Typography */
--text-xs: 11px;
--text-sm: 13px;
@@ -71,7 +71,7 @@
--leading-tight: 1.2;
--leading-normal: 1.5;
--leading-relaxed: 1.7;
/* Transitions */
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
@@ -80,6 +80,50 @@
--duration-slow: 350ms;
}
[data-theme="dark"] {
/* Background */
--bg: #101114;
--bg-raised: #141518;
--surface: #1a1b1f;
--surface-hover: #212229;
--surface-active: #282a33;
/* Border */
--border: #2c2d34;
--border-strong: #3d3f48;
--border-subtle: #232429;
/* Foreground / text */
--fg: #e4e5e9;
--fg-secondary: #a0a3ad;
--muted: #5f6270;
--faint: #43454f;
/* Accent */
--accent: #8a9dff;
--accent-hover: #9dacff;
--accent-strong: #7088ff;
--accent-bg: rgba(138, 157, 255, 0.08);
--accent-bg-hover: rgba(138, 157, 255, 0.14);
/* Semantic */
--success: #5ec98f;
--success-bg: rgba(94, 201, 143, 0.08);
--warning: #e0a855;
--warning-bg: rgba(224, 168, 85, 0.08);
--error: #e06565;
--error-bg: rgba(224, 101, 101, 0.08);
--info: #60a5d6;
--info-bg: rgba(96, 165, 214, 0.08);
/* Overlay / depth */
--overlay: rgba(0, 0, 0, 0.55);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.30);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.35);
}
*,
*::before,
*::after {
@@ -104,4 +148,4 @@ body {
h1 {
font-size: 32px !important;
}
}
}

View File

@@ -2,6 +2,8 @@ import type { Metadata } from "next";
import { JetBrains_Mono, DM_Sans } from "next/font/google";
import { SITE } from "@/data/content";
import "./globals.css";
import { ThemeProvider } from 'next-themes'
import ThemeSwitch from "@/components/ThemeSwitch";
const mono = JetBrains_Mono({
subsets: ["latin"],
@@ -26,8 +28,14 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="en" className={`${mono.variable} ${sans.variable}`}>
<body>{children}</body>
<html lang="en" className={`${mono.variable} ${sans.variable}`} suppressHydrationWarning>
<head />
<body>
<ThemeProvider attribute="data-theme">
<ThemeSwitch/>
{children}
</ThemeProvider>
</body>
</html>
);
}

View File

@@ -24,7 +24,7 @@ export default function Footer() {
>
<div style={{ display: "flex", gap: 16 }}>
<span>
(2026) - Hunter W.
(2026) - Hunter W. - Powered by Next.JS
</span>
</div>
<div style={{ display: "flex", gap: 16 }}>

View File

@@ -0,0 +1,95 @@
'use client'
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
const themes = ['system', 'light', 'dark'] as const
const ThemeSwitch = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) return null
const next = () => {
const i = themes.indexOf(theme as (typeof themes)[number])
setTheme(themes[(i + 1) % themes.length])
}
return (
<button
onClick={next}
aria-label={`Theme: ${theme}`}
title={`Theme: ${theme}`}
style={{
position: 'fixed',
top: 'var(--space-4)',
right: 'var(--space-4)',
zIndex: 9999,
background: 'var(--surface)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius-md)',
color: 'var(--fg-secondary)',
cursor: 'pointer',
padding: '8px',
display: 'grid',
placeItems: 'center',
transition: `background var(--duration-fast) var(--ease-out),
border-color var(--duration-fast) var(--ease-out),
color var(--duration-fast) var(--ease-out)`,
}}
onMouseEnter={e => {
e.currentTarget.style.background = 'var(--surface-hover)'
e.currentTarget.style.borderColor = 'var(--border-strong)'
e.currentTarget.style.color = 'var(--fg)'
}}
onMouseLeave={e => {
e.currentTarget.style.background = 'var(--surface)'
e.currentTarget.style.borderColor = 'var(--border)'
e.currentTarget.style.color = 'var(--fg-secondary)'
}}
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
>
{theme === 'dark' ? (
// Moon
<path d="M21 12.79A9 9 0 1 1 11.21 3a7 7 0 0 0 9.79 9.79z" />
) : theme === 'light' ? (
// Sun
<>
<circle cx="12" cy="12" r="5" />
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</>
) : (
// Monitor (system)
<>
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
<line x1="8" y1="21" x2="16" y2="21" />
<line x1="12" y1="17" x2="12" y2="21" />
</>
)}
</svg>
</button>
)
}
export default ThemeSwitch

11
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"next": "16.2.1",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
@@ -5048,6 +5049,16 @@
}
}
},
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",

View File

@@ -11,7 +11,8 @@
"dependencies": {
"next": "16.2.1",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"next-themes": "^0.4.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

10
pages/_app.tsx Normal file
View File

@@ -0,0 +1,10 @@
import type { AppProps } from 'next/app'
import { ThemeProvider } from 'next-themes'
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
)
}