added dark/light mode and control
This commit is contained in:
120
app/globals.css
120
app/globals.css
@@ -1,5 +1,87 @@
|
||||
:root {
|
||||
|
||||
/* Background */
|
||||
--bg: #ffffff;
|
||||
--bg-raised: #f8f8fa;
|
||||
--surface: #f1f2f4;
|
||||
--surface-hover: #e8e9ed;
|
||||
--surface-active: #dfe0e5;
|
||||
|
||||
/* Border */
|
||||
--border: #d4d5db;
|
||||
--border-strong: #b8bac2;
|
||||
--border-subtle: #e4e5ea;
|
||||
|
||||
/* Foreground / text */
|
||||
--fg: #131419;
|
||||
--fg-secondary: #4e5264;
|
||||
--muted: #7e8290;
|
||||
--faint: #b0b3bd;
|
||||
|
||||
/* Accent */
|
||||
--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: #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.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;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* Typography */
|
||||
--text-xs: 11px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 15px;
|
||||
--text-lg: 18px;
|
||||
--text-xl: 22px;
|
||||
--text-2xl: 28px;
|
||||
--text-3xl: 36px;
|
||||
--text-4xl: 42px;
|
||||
--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);
|
||||
--duration-fast: 120ms;
|
||||
--duration-normal: 200ms;
|
||||
--duration-slow: 350ms;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
|
||||
/* Background */
|
||||
--bg: #101114;
|
||||
--bg-raised: #141518;
|
||||
@@ -40,44 +122,6 @@
|
||||
--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);
|
||||
|
||||
/* Radii */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 10px;
|
||||
--radius-xl: 16px;
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 24px;
|
||||
--space-8: 32px;
|
||||
--space-10: 40px;
|
||||
--space-12: 48px;
|
||||
--space-16: 64px;
|
||||
|
||||
/* Typography */
|
||||
--text-xs: 11px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 15px;
|
||||
--text-lg: 18px;
|
||||
--text-xl: 22px;
|
||||
--text-2xl: 28px;
|
||||
--text-3xl: 36px;
|
||||
--text-4xl: 42px;
|
||||
--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);
|
||||
--duration-fast: 120ms;
|
||||
--duration-normal: 200ms;
|
||||
--duration-slow: 350ms;
|
||||
}
|
||||
|
||||
*,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
95
components/ThemeSwitch.tsx
Normal file
95
components/ThemeSwitch.tsx
Normal 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
11
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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
10
pages/_app.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user