feat: Add light mode toggle and implementation - #114
alastairong1 wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis pull request adds comprehensive theming support to the application with a light/dark mode toggle. It introduces new React components for layout and theme switching, integrates theme initialization into the Next.js app lifecycle with localStorage persistence, configures Tailwind CSS with custom color palettes and dark mode support, and adds global styling with transitions and theme-aware scrollbars. Changes
Estimated Code Review Effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@components/Layout.jsx`:
- Around line 4-26: The top-level container isn't a flex container so the <main>
element's flex-1 has no effect; update the wrapping div that contains <nav> and
<main> (the div with classes "min-h-screen bg-white dark:bg-gray-900") to be a
column flex container (add "flex flex-col") so that <main className="flex-1">
grows to fill remaining space; ensure the nav remains above and children render
inside the flex column.
In `@components/ThemeToggle.jsx`:
- Around line 4-17: The component initializes theme state eagerly with
useState(true) causing a flash; change to lazy initialization by passing a
function to useState that reads localStorage (e.g., useState(() => { const s =
localStorage.getItem('theme'); return s ? s === 'dark' : true; })) so isDark
reflects the persisted preference on first render and remove the initial forced
set in the useEffect; ensure document.documentElement.classList is updated once
on mount (in the existing useEffect) based on that initialized isDark value to
avoid UI flicker.
In `@pages/_app.jsx`:
- Around line 5-13: Replace the client-side theme init in the useEffect inside
your App component with a blocking inline script injected in the server document
so theme is set before hydration: create/update the Document component (function
Document) to include a script element before Main that reads
localStorage.getItem('theme') and adds/removes the 'dark' class on
document.documentElement accordingly, then remove or simplify the useEffect in
pages/_app.jsx so it no longer toggles the class after hydration.
🧹 Nitpick comments (5)
tailwind.config.js (1)
11-20: Customlightcolor palette is defined but unused.The custom color palette under
light(primary, secondary, background, surface, text) is not used anywhere in the components reviewed. All components use standard Tailwind colors likegray-*,indigo-*withdark:variants instead.Consider either:
- Using these custom colors in your components for consistency (e.g.,
bg-light-backgroundinstead ofbg-white)- Removing this unused configuration to avoid confusion
components/Layout.jsx (1)
15-20: Use Next.jsLinkfor client-side navigation.Using plain
<a>tags will cause full page reloads, losing client-side navigation benefits and potentially causing theme flash on each navigation.♻️ Proposed fix
+import Link from 'next/link'; + // Example of updating a common layout component export default function Layout({ children }) { return ( ... {/* Nav items */} <div className="flex items-center space-x-4"> - <a href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"> + <Link href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"> Dashboard - </a> - <a href="/dashboard/settings" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"> + </Link> + <Link href="/dashboard/settings" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"> Settings - </a> + </Link> </div>styles/globals.css (2)
5-8: Universal transition on all elements may impact performance.Applying
transition-colorsto every element (*) can cause performance issues on complex pages with many DOM nodes. Consider scoping this to specific elements or using a more targeted approach.♻️ Proposed scoped approach
-/* Base styles for smooth theme transitions */ -* { - `@apply` transition-colors duration-200; -} +/* Base styles for smooth theme transitions */ +body, +nav, +main, +.theme-transition { + `@apply` transition-colors duration-200; +}Alternatively, add
.theme-transitionclass to elements that need smooth color changes.
1-3: Consider using@tailwinddirectives instead of@import.While
@import 'tailwindcss/...'works, the idiomatic Tailwind approach uses@tailwinddirectives. This also resolves the Biome static analysis warnings (which are false positives for valid Tailwind CSS but indicate the linter config may need updating).♻️ Idiomatic Tailwind syntax
-@import 'tailwindcss/base'; -@import 'tailwindcss/components'; -@import 'tailwindcss/utilities'; +@tailwind base; +@tailwind components; +@tailwind utilities;Note: You may also need to configure Biome to recognize Tailwind directives by enabling
tailwindDirectivesin the CSS parser options.pages/_app.jsx (1)
5-13: Duplicate theme initialization logic.This theme logic is duplicated in
ThemeToggle.jsx(Lines 6-16). Consider centralizing theme management in a custom hook or context to avoid inconsistencies and reduce maintenance burden.
| <div className="min-h-screen bg-white dark:bg-gray-900"> | ||
| <nav className="bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700"> | ||
| {/* Navigation content with light/dark mode support */} | ||
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | ||
| <div className="flex justify-between h-16"> | ||
| {/* Logo */} | ||
| <div className="flex items-center"> | ||
| <h1 className="text-xl font-bold text-gray-900 dark:text-white">st0x</h1> | ||
| </div> | ||
| {/* Nav items */} | ||
| <div className="flex items-center space-x-4"> | ||
| <a href="/dashboard" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"> | ||
| Dashboard | ||
| </a> | ||
| <a href="/dashboard/settings" className="text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white"> | ||
| Settings | ||
| </a> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </nav> | ||
| <main className="flex-1">{children}</main> | ||
| </div> |
There was a problem hiding this comment.
flex-1 on <main> has no effect without flex container.
The flex-1 class on Line 25 won't work because the parent <div> doesn't have display: flex. The main content won't expand to fill remaining height.
🛠️ Proposed fix
- <div className="min-h-screen bg-white dark:bg-gray-900">
+ <div className="min-h-screen flex flex-col bg-white dark:bg-gray-900">🤖 Prompt for AI Agents
In `@components/Layout.jsx` around lines 4 - 26, The top-level container isn't a
flex container so the <main> element's flex-1 has no effect; update the wrapping
div that contains <nav> and <main> (the div with classes "min-h-screen bg-white
dark:bg-gray-900") to be a column flex container (add "flex flex-col") so that
<main className="flex-1"> grows to fill remaining space; ensure the nav remains
above and children render inside the flex column.
| const [isDark, setIsDark] = useState(true); | ||
|
|
||
| useEffect(() => { | ||
| // Check for saved theme preference or default to dark mode | ||
| const savedTheme = localStorage.getItem('theme'); | ||
| const prefersDark = savedTheme ? savedTheme === 'dark' : true; | ||
| setIsDark(prefersDark); | ||
|
|
||
| if (prefersDark) { | ||
| document.documentElement.classList.add('dark'); | ||
| } else { | ||
| document.documentElement.classList.remove('dark'); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
Initial state mismatch causes toggle UI flash.
The initial useState(true) doesn't account for the actual stored preference. On first render, the toggle will show dark mode, then potentially flip to light mode after useEffect runs, causing a visual flash.
Consider initializing state lazily or using a loading state:
🛠️ Proposed fix with lazy initialization
export default function ThemeToggle() {
- const [isDark, setIsDark] = useState(true);
+ const [isDark, setIsDark] = useState(() => {
+ if (typeof window === 'undefined') return true;
+ const savedTheme = localStorage.getItem('theme');
+ return savedTheme ? savedTheme === 'dark' : true;
+ });
useEffect(() => {
- // Check for saved theme preference or default to dark mode
- const savedTheme = localStorage.getItem('theme');
- const prefersDark = savedTheme ? savedTheme === 'dark' : true;
- setIsDark(prefersDark);
-
- if (prefersDark) {
+ // Apply theme class on mount
+ if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
- }, []);
+ }, [isDark]);🤖 Prompt for AI Agents
In `@components/ThemeToggle.jsx` around lines 4 - 17, The component initializes
theme state eagerly with useState(true) causing a flash; change to lazy
initialization by passing a function to useState that reads localStorage (e.g.,
useState(() => { const s = localStorage.getItem('theme'); return s ? s ===
'dark' : true; })) so isDark reflects the persisted preference on first render
and remove the initial forced set in the useEffect; ensure
document.documentElement.classList is updated once on mount (in the existing
useEffect) based on that initialized isDark value to avoid UI flicker.
| useEffect(() => { | ||
| // Initialize theme on app load | ||
| const savedTheme = localStorage.getItem('theme'); | ||
| if (savedTheme === 'light') { | ||
| document.documentElement.classList.remove('dark'); | ||
| } else { | ||
| document.documentElement.classList.add('dark'); | ||
| } | ||
| }, []); |
There was a problem hiding this comment.
Theme initialization in useEffect causes flash of incorrect theme (FOUC).
Since useEffect runs after hydration, users will briefly see the wrong theme before JavaScript applies the correct class. This is especially noticeable on slow connections or when the saved theme differs from the default.
The standard fix is to add a blocking inline script in _document.jsx that runs before React hydrates:
🛠️ Recommended fix using _document.jsx
Create or update pages/_document.jsx:
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head />
<body>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
const theme = localStorage.getItem('theme');
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
})();
`,
}}
/>
<Main />
<NextScript />
</body>
</Html>
);
}Then you can simplify _app.jsx:
-import { useEffect } from 'react';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
- useEffect(() => {
- // Initialize theme on app load
- const savedTheme = localStorage.getItem('theme');
- if (savedTheme === 'light') {
- document.documentElement.classList.remove('dark');
- } else {
- document.documentElement.classList.add('dark');
- }
- }, []);
-
return <Component {...pageProps} />;
}🤖 Prompt for AI Agents
In `@pages/_app.jsx` around lines 5 - 13, Replace the client-side theme init in
the useEffect inside your App component with a blocking inline script injected
in the server document so theme is set before hydration: create/update the
Document component (function Document) to include a script element before Main
that reads localStorage.getItem('theme') and adds/removes the 'dark' class on
document.documentElement accordingly, then remove or simplify the useEffect in
pages/_app.jsx so it no longer toggles the class after hydration.
Description
This PR implements a light mode theme for the st0x application with a toggle in the user dashboard/settings.
Changes
Implementation Details
Testing
Reasoning:
Since the task description mentions implementing a light mode toggle but the actual task name refers to updating to use localDB, I'm focusing on what's explicitly described in the page content - implementing a light mode toggle. The implementation uses Tailwind's built-in dark mode support with the 'class' strategy, allowing for easy theme switching. The theme preference is stored in localStorage for persistence across sessions. All components are updated to support both light and dark themes using Tailwind's dark: prefix for conditional styling.
Generated by AI Project Manager
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.