From c6333fefa97c335af8e1e856dfed451164e6c1ae Mon Sep 17 00:00:00 2001
From: punjitha <132387971+algotyrnt@users.noreply.github.com>
Date: Fri, 15 May 2026 09:14:57 +0530
Subject: [PATCH 1/6] Add theme toggle functionality and update styles for dark
mode support
---
package-lock.json | 11 +++
package.json | 1 +
src/app/globals.css | 39 +++++++++-
src/app/layout.tsx | 5 +-
src/components/layout/header.tsx | 13 +++-
src/components/theme/registry.tsx | 43 +++++++++--
src/components/theme/theme-toggle.tsx | 62 +++++++++++++++
src/components/theme/theme.ts | 104 +++++++++++++-------------
src/components/theme/tokens.ts | 14 ++--
9 files changed, 223 insertions(+), 69 deletions(-)
create mode 100644 src/components/theme/theme-toggle.tsx
diff --git a/package-lock.json b/package-lock.json
index 5401c11..e0c5e19 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"next": "^16.2.6",
+ "next-themes": "^0.4.6",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
@@ -5158,6 +5159,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/node-exports-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
diff --git a/package.json b/package.json
index 344b7a9..5055376 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"@vercel/analytics": "^2.0.1",
"@vercel/speed-insights": "^2.0.0",
"next": "^16.2.6",
+ "next-themes": "^0.4.6",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
diff --git a/src/app/globals.css b/src/app/globals.css
index 48a5f1a..e24e41f 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,3 +1,29 @@
+:root {
+ --palette-primary: #09090b;
+ --shadow-card: 0 2px 12px rgba(0, 0, 0, 0.06);
+ --shadow-card-hover: 0 6px 24px rgba(0, 0, 0, 0.07);
+ --shadow-social-hover: 0 2px 8px rgba(0, 0, 0, 0.06);
+ --border-subtle: rgba(0, 0, 0, 0.08);
+ --border-medium: rgba(0, 0, 0, 0.14);
+ --backdrop-header: rgba(252, 252, 252, 0.85);
+ --selection-bg: rgba(9, 9, 11, 0.08);
+ --scrollbar-thumb: rgba(0, 0, 0, 0.12);
+ --scrollbar-thumb-hover: rgba(0, 0, 0, 0.2);
+}
+
+[class~='dark'] {
+ --palette-primary: #ffffff;
+ --shadow-card: none;
+ --shadow-card-hover: none;
+ --shadow-social-hover: 0 2px 8px rgba(0, 0, 0, 0.2);
+ --border-subtle: rgba(255, 255, 255, 0.08);
+ --border-medium: rgba(255, 255, 255, 0.14);
+ --backdrop-header: rgba(9, 9, 11, 0.85);
+ --selection-bg: rgba(255, 255, 255, 0.08);
+ --scrollbar-thumb: rgba(255, 255, 255, 0.12);
+ --scrollbar-thumb-hover: rgba(255, 255, 255, 0.2);
+}
+
/* Minimal CSS for Next.js and MUI */
html,
body {
@@ -14,10 +40,15 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
+ background-color: #fcfcfc;
+}
+
+[class~='dark'] body {
+ background-color: #09090b;
}
::selection {
- background: rgba(9, 9, 11, 0.08);
+ background: var(--selection-bg);
}
/* Thin scrollbar */
@@ -30,12 +61,12 @@ body {
}
::-webkit-scrollbar-thumb {
- background: rgba(0, 0, 0, 0.12);
+ background: var(--scrollbar-thumb);
border-radius: 2px;
}
::-webkit-scrollbar-thumb:hover {
- background: rgba(0, 0, 0, 0.2);
+ background: var(--scrollbar-thumb-hover);
}
@media (prefers-color-scheme: dark) {
@@ -133,4 +164,4 @@ body {
max-width: 600px;
flex: 1;
padding: 0 20px;
-}
\ No newline at end of file
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6485ffb..eb1d518 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -19,7 +19,10 @@ import {
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
- themeColor: '#fcfcfc',
+ themeColor: [
+ { media: '(prefers-color-scheme: light)', color: '#fcfcfc' },
+ { media: '(prefers-color-scheme: dark)', color: '#09090b' },
+ ],
}
export const metadata: Metadata = {
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx
index 07909de..1b77e5a 100644
--- a/src/components/layout/header.tsx
+++ b/src/components/layout/header.tsx
@@ -3,6 +3,7 @@ import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import Stack from '@mui/material/Stack'
import { tokens } from '@/components/theme/tokens'
+import { ThemeToggle } from '@/components/theme/theme-toggle'
const NAV_LINKS = [
{ label: 'Work', href: '#work' },
@@ -58,7 +59,7 @@ export function Header() {
diff --git a/src/components/theme/registry.tsx b/src/components/theme/registry.tsx
index 172cb17..fc704c6 100644
--- a/src/components/theme/registry.tsx
+++ b/src/components/theme/registry.tsx
@@ -1,9 +1,41 @@
'use client'
import * as React from 'react'
-import { ThemeProvider } from '@mui/material/styles'
+import { ThemeProvider as MuiThemeProvider } from '@mui/material/styles'
import CssBaseline from '@mui/material/CssBaseline'
+import { ThemeProvider as NextThemesProvider, useTheme } from 'next-themes'
import NextAppDirEmotionCacheProvider from './emotion-cache'
-import theme from './theme'
+import { getTheme } from './theme'
+
+function MuiThemeWrapper({ children }: { children: React.ReactNode }) {
+ const { theme, resolvedTheme } = useTheme()
+ const [mounted, setMounted] = React.useState(false)
+
+ // Avoid hydration mismatch
+ React.useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ const currentTheme = React.useMemo(() => {
+ const mode = (resolvedTheme || theme || 'light') as 'light' | 'dark'
+ return getTheme(mode)
+ }, [theme, resolvedTheme])
+
+ if (!mounted) {
+ return (
+
+
+ {children}
+
+ )
+ }
+
+ return (
+
+
+ {children}
+
+ )
+}
export default function ThemeRegistry({
children,
@@ -12,10 +44,9 @@ export default function ThemeRegistry({
}) {
return (
-
-
- {children}
-
+
+ {children}
+
)
}
diff --git a/src/components/theme/theme-toggle.tsx b/src/components/theme/theme-toggle.tsx
new file mode 100644
index 0000000..2f63ba2
--- /dev/null
+++ b/src/components/theme/theme-toggle.tsx
@@ -0,0 +1,62 @@
+'use client'
+import * as React from 'react'
+import { useTheme } from 'next-themes'
+import IconButton from '@mui/material/IconButton'
+import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'
+import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'
+import { tokens } from './tokens'
+
+export function ThemeToggle() {
+ const { setTheme, resolvedTheme } = useTheme()
+ const [mounted, setMounted] = React.useState(false)
+
+ // Avoid hydration mismatch
+ React.useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ if (!mounted) {
+ return (
+
+
+
+ )
+ }
+
+ const isDark = resolvedTheme === 'dark'
+
+ return (
+ setTheme(isDark ? 'light' : 'dark')}
+ aria-label="Toggle theme"
+ sx={{
+ color: 'text.disabled',
+ p: 0.75,
+ borderRadius: '8px',
+ border: '1px solid',
+ borderColor: tokens.border.subtle,
+ transition: 'all 0.15s ease',
+ '&:hover': {
+ color: 'text.primary',
+ bgcolor: 'rgba(0,0,0,0.04)',
+ borderColor: tokens.border.medium,
+ },
+ '& svg': {
+ fontSize: 18,
+ },
+ }}
+ >
+ {isDark ? : }
+
+ )
+}
diff --git a/src/components/theme/theme.ts b/src/components/theme/theme.ts
index 2b59da8..f942ba2 100644
--- a/src/components/theme/theme.ts
+++ b/src/components/theme/theme.ts
@@ -1,61 +1,65 @@
-import { createTheme } from '@mui/material/styles'
-import { tokens } from './tokens'
+import { createTheme, PaletteMode } from '@mui/material/styles'
-const theme = createTheme({
- palette: {
- mode: 'light',
- primary: {
- main: tokens.palette.primaryMain,
- },
- background: {
- default: '#f8f8f8',
- paper: '#ffffff',
- },
- text: {
- primary: '#0f0f11',
- secondary: '#5c5c6e',
- disabled: '#9898a6',
- },
- divider: tokens.border.subtle,
- },
- typography: {
- fontFamily: 'var(--font-inter), "Inter", "Helvetica", "Arial", sans-serif',
- button: {
- textTransform: 'none',
- fontWeight: 500,
+export const getTheme = (mode: PaletteMode) => {
+ const isLight = mode === 'light'
+
+ return createTheme({
+ palette: {
+ mode,
+ primary: {
+ main: isLight ? '#09090b' : '#ffffff',
+ },
+ background: {
+ default: isLight ? '#fcfcfc' : '#09090b',
+ paper: isLight ? '#ffffff' : '#111113',
+ },
+ text: {
+ primary: isLight ? '#0f0f11' : '#f8f8f8',
+ secondary: isLight ? '#5c5c6e' : '#a1a1aa',
+ disabled: isLight ? '#9898a6' : '#52525b',
+ },
+ divider: isLight ? 'rgba(0,0,0,0.08)' : 'rgba(255, 255, 255, 0.08)',
},
- },
- components: {
- MuiButtonBase: {
- defaultProps: {
- disableRipple: true,
+ typography: {
+ fontFamily:
+ 'var(--font-inter), "Inter", "Helvetica", "Arial", sans-serif',
+ button: {
+ textTransform: 'none',
+ fontWeight: 500,
},
},
- MuiButton: {
- styleOverrides: {
- root: {
- borderRadius: 12,
- boxShadow: 'none',
- '&:hover': {
+ components: {
+ MuiButtonBase: {
+ defaultProps: {
+ disableRipple: true,
+ },
+ },
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ borderRadius: 12,
boxShadow: 'none',
+ '&:hover': {
+ boxShadow: 'none',
+ },
},
},
},
- },
- MuiPaper: {
- styleOverrides: {
- root: {
- backgroundImage: 'none',
- boxShadow: tokens.shadow.card,
- border: `1px solid ${tokens.border.subtle}`,
- borderRadius: 16,
+ MuiPaper: {
+ styleOverrides: {
+ root: {
+ backgroundImage: 'none',
+ boxShadow: isLight ? '0 2px 12px rgba(0,0,0,0.06)' : 'none',
+ border: `1px solid ${
+ isLight ? 'rgba(0,0,0,0.08)' : 'rgba(255, 255, 255, 0.08)'
+ }`,
+ borderRadius: 16,
+ },
+ },
+ defaultProps: {
+ elevation: 0,
},
- },
- defaultProps: {
- elevation: 0,
},
},
- },
-})
-
-export default theme
+ })
+}
diff --git a/src/components/theme/tokens.ts b/src/components/theme/tokens.ts
index c124b74..8679966 100644
--- a/src/components/theme/tokens.ts
+++ b/src/components/theme/tokens.ts
@@ -7,18 +7,18 @@
*/
export const tokens = {
palette: {
- primaryMain: '#09090b',
+ primaryMain: 'var(--palette-primary)',
},
shadow: {
- card: '0 2px 12px rgba(0,0,0,0.06)',
- cardHover: '0 6px 24px rgba(0,0,0,0.07)',
- socialHover: '0 2px 8px rgba(0,0,0,0.06)',
+ card: 'var(--shadow-card)',
+ cardHover: 'var(--shadow-card-hover)',
+ socialHover: 'var(--shadow-social-hover)',
},
border: {
- subtle: 'rgba(0,0,0,0.08)',
- medium: 'rgba(0,0,0,0.14)',
+ subtle: 'var(--border-subtle)',
+ medium: 'var(--border-medium)',
},
backdrop: {
- header: 'rgba(248, 248, 248, 0.85)',
+ header: 'var(--backdrop-header)',
},
} as const
From c2f3ae04215bf527786e7ef6f5fa36e445a49b01 Mon Sep 17 00:00:00 2001
From: punjitha <132387971+algotyrnt@users.noreply.github.com>
Date: Fri, 15 May 2026 11:10:01 +0530
Subject: [PATCH 2/6] Adjust header spacing and simplify theme hydration
handling by removing mount state checks
---
src/components/layout/header.tsx | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx
index 1b77e5a..9129f6c 100644
--- a/src/components/layout/header.tsx
+++ b/src/components/layout/header.tsx
@@ -59,7 +59,7 @@ export function Header() {