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() {