feat(ui): add dark mode toggle and stabilize docs sidebar on mobile#1
feat(ui): add dark mode toggle and stabilize docs sidebar on mobile#10xEgao wants to merge 1 commit intocitadel-tech:mainfrom
Conversation
📝 WalkthroughWalkthroughThis PR introduces a dark theme feature with localStorage persistence. The Header component now includes theme toggle functionality with UI updates for both desktop and mobile layouts. Comprehensive dark mode CSS variables and styling are added to override colors and component-specific styles. The Docs component is updated with improved sidebar scroll behavior and accessibility enhancements. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 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)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/components/layout/Header.jsx`:
- Around line 15-22: The page can show a wrong theme briefly because
resolveInitialTheme runs only during React hydration; add a small blocking
inline script in your HTML <head> that reads the same storage key
(THEME_STORAGE_KEY) and applies it to document.documentElement (set a data-theme
attribute and/or toggle the "dark" class) before the React bundle loads, falling
back to matchMedia('(prefers-color-scheme: dark)') if the stored value is absent
or invalid; keep resolveInitialTheme as-is so React reads the same source of
truth on hydration.
- Around line 74-78: The effect in useEffect currently always writes the
resolved theme to localStorage (THEME_STORAGE_KEY) on mount/changes which
overrides system-preference users; modify the logic to write to localStorage
only when the user has explicitly chosen a theme: add a piece of state/prop like
hasUserChosenTheme (or isUserThemePreference) and update that flag when the user
uses the theme toggle handler, then change the useEffect that calls
document.documentElement.setAttribute and classList.toggle to always set DOM
theme but call window.localStorage.setItem(THEME_STORAGE_KEY, theme) only if
hasUserChosenTheme is true so initial mount respects system preference.
In `@src/index.css`:
- Around line 119-148: The dark-mode CSS block repeats many !important overrides
for selectors like html[data-theme='dark'] .text-black and
.border-black\/35/.bg-white\/30; replace these per-utility overrides by defining
a semantic RGB CSS variable (e.g. --text-primary and --border-primary) inside
html[data-theme='dark'] and :root, then remove the repeated !important rules and
update components to use Tailwind arbitrary colors that reference the variables
(e.g. text-[rgb(var(--text-primary)/0.8)] or
border-[rgb(var(--border-primary)/0.35)]), allowing Tailwind opacity modifiers
to work without specificity hacks.
In `@src/pages/Docs.jsx`:
- Around line 21-28: Docs.jsx and Header.jsx both directly mutate
document.body.style.overflow causing cleanup race conditions; extract and use a
shared hook (e.g., create useScrollLock) that accepts a boolean (use it with
Docs' sidebarOpen and Header's open) to centrally increment/decrement a lock
count, set overflow='hidden' when count>0 and only restore overflow when count
reaches 0, then replace the existing direct body.style.overflow effects in both
components with calls to useScrollLock(sidebarOpen) and useScrollLock(open).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 21b71928-49a1-4994-bb15-87a7467091b2
📒 Files selected for processing (3)
src/components/layout/Header.jsxsrc/index.csssrc/pages/Docs.jsx
| function resolveInitialTheme() { | ||
| if (typeof window === 'undefined') return 'light' | ||
|
|
||
| const stored = window.localStorage.getItem(THEME_STORAGE_KEY) | ||
| if (stored === 'light' || stored === 'dark') return stored | ||
|
|
||
| return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light' | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Potential flash of unstyled content (FOUC) on page load.
The theme is resolved in React state after hydration, which means users may briefly see the wrong theme before React runs. For a smoother experience, consider adding a blocking script in the HTML <head> that sets data-theme before the page renders.
💡 Inline script to prevent theme flash
Add this script to your index.html before the React bundle loads:
<script>
(function() {
var stored = localStorage.getItem('coinswap-theme');
var theme = stored === 'light' || stored === 'dark'
? stored
: (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
})();
</script>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/layout/Header.jsx` around lines 15 - 22, The page can show a
wrong theme briefly because resolveInitialTheme runs only during React
hydration; add a small blocking inline script in your HTML <head> that reads the
same storage key (THEME_STORAGE_KEY) and applies it to document.documentElement
(set a data-theme attribute and/or toggle the "dark" class) before the React
bundle loads, falling back to matchMedia('(prefers-color-scheme: dark)') if the
stored value is absent or invalid; keep resolveInitialTheme as-is so React reads
the same source of truth on hydration.
| useEffect(() => { | ||
| document.documentElement.setAttribute('data-theme', theme) | ||
| document.documentElement.classList.toggle('dark', theme === 'dark') | ||
| window.localStorage.setItem(THEME_STORAGE_KEY, theme) | ||
| }, [theme]) |
There was a problem hiding this comment.
Theme effect writes to localStorage on every theme change, including initial mount.
On initial render, this writes the resolved theme to localStorage even if the user never explicitly chose a preference. This may override the system preference behavior for users who expect automatic theme switching based on OS settings.
🛡️ Proposed fix to preserve system preference
Track whether the user has explicitly chosen a theme:
+const THEME_STORAGE_KEY = 'coinswap-theme'
+
+function resolveInitialTheme() {
+ if (typeof window === 'undefined') return 'light'
+
+ const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
+ if (stored === 'light' || stored === 'dark') return stored
+
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
+}
+
+function hasStoredPreference() {
+ if (typeof window === 'undefined') return false
+ const stored = window.localStorage.getItem(THEME_STORAGE_KEY)
+ return stored === 'light' || stored === 'dark'
+}
// In component:
- useEffect(() => {
- document.documentElement.setAttribute('data-theme', theme)
- document.documentElement.classList.toggle('dark', theme === 'dark')
- window.localStorage.setItem(THEME_STORAGE_KEY, theme)
- }, [theme])
+ const [hasExplicitChoice, setHasExplicitChoice] = useState(hasStoredPreference)
+
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', theme)
+ document.documentElement.classList.toggle('dark', theme === 'dark')
+ if (hasExplicitChoice) {
+ window.localStorage.setItem(THEME_STORAGE_KEY, theme)
+ }
+ }, [theme, hasExplicitChoice])
+
+ function toggleTheme() {
+ setHasExplicitChoice(true)
+ setTheme(current => (current === 'dark' ? 'light' : 'dark'))
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/layout/Header.jsx` around lines 74 - 78, The effect in
useEffect currently always writes the resolved theme to localStorage
(THEME_STORAGE_KEY) on mount/changes which overrides system-preference users;
modify the logic to write to localStorage only when the user has explicitly
chosen a theme: add a piece of state/prop like hasUserChosenTheme (or
isUserThemePreference) and update that flag when the user uses the theme toggle
handler, then change the useEffect that calls
document.documentElement.setAttribute and classList.toggle to always set DOM
theme but call window.localStorage.setItem(THEME_STORAGE_KEY, theme) only if
hasUserChosenTheme is true so initial mount respects system preference.
| html[data-theme='dark'] .text-black { color: #edf3ff !important; } | ||
| html[data-theme='dark'] .text-black\/80 { color: rgb(237 243 255 / 0.8) !important; } | ||
| html[data-theme='dark'] .text-black\/78 { color: rgb(237 243 255 / 0.78) !important; } | ||
| html[data-theme='dark'] .text-black\/70 { color: rgb(237 243 255 / 0.7) !important; } | ||
| html[data-theme='dark'] .text-black\/65 { color: rgb(237 243 255 / 0.65) !important; } | ||
| html[data-theme='dark'] .text-black\/60 { color: rgb(237 243 255 / 0.6) !important; } | ||
| html[data-theme='dark'] .text-black\/55 { color: rgb(237 243 255 / 0.55) !important; } | ||
| html[data-theme='dark'] .text-black\/52 { color: rgb(237 243 255 / 0.52) !important; } | ||
| html[data-theme='dark'] .text-black\/50 { color: rgb(237 243 255 / 0.5) !important; } | ||
| html[data-theme='dark'] .text-black\/45 { color: rgb(237 243 255 / 0.45) !important; } | ||
| html[data-theme='dark'] .text-black\/42 { color: rgb(237 243 255 / 0.42) !important; } | ||
| html[data-theme='dark'] .text-black\/35 { color: rgb(237 243 255 / 0.35) !important; } | ||
| html[data-theme='dark'] .text-black\/30 { color: rgb(237 243 255 / 0.3) !important; } | ||
| html[data-theme='dark'] .text-black\/28 { color: rgb(237 243 255 / 0.28) !important; } | ||
|
|
||
| html[data-theme='dark'] .border-black\/35 { border-color: rgb(237 243 255 / 0.35) !important; } | ||
| html[data-theme='dark'] .border-black\/25 { border-color: rgb(237 243 255 / 0.25) !important; } | ||
| html[data-theme='dark'] .border-black\/20 { border-color: rgb(237 243 255 / 0.2) !important; } | ||
| html[data-theme='dark'] .border-black\/15 { border-color: rgb(237 243 255 / 0.15) !important; } | ||
| html[data-theme='dark'] .border-black\/12 { border-color: rgb(237 243 255 / 0.12) !important; } | ||
| html[data-theme='dark'] .border-black\/10 { border-color: rgb(237 243 255 / 0.1) !important; } | ||
| html[data-theme='dark'] .border-black\/8 { border-color: rgb(237 243 255 / 0.08) !important; } | ||
|
|
||
| html[data-theme='dark'] .bg-white\/30 { background-color: rgb(255 255 255 / 0.08) !important; } | ||
| html[data-theme='dark'] .bg-white\/25 { background-color: rgb(255 255 255 / 0.07) !important; } | ||
| html[data-theme='dark'] .bg-white\/20 { background-color: rgb(255 255 255 / 0.06) !important; } | ||
| html[data-theme='dark'] .bg-white\/15 { background-color: rgb(255 255 255 / 0.05) !important; } | ||
| html[data-theme='dark'] .bg-white\/12 { background-color: rgb(255 255 255 / 0.05) !important; } | ||
| html[data-theme='dark'] .bg-black\/8 { background-color: rgb(255 255 255 / 0.08) !important; } | ||
| html[data-theme='dark'] .bg-black\/4 { background-color: rgb(255 255 255 / 0.05) !important; } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider reducing !important overrides with a more maintainable approach.
The repeated !important declarations for each opacity variant create maintenance overhead and specificity conflicts. Consider using CSS custom properties for the base color that can be switched in dark mode, allowing Tailwind's opacity modifiers to work naturally.
♻️ Alternative approach using CSS custom properties
Instead of overriding each utility individually, define a semantic color variable:
:root {
--text-primary: 23 23 23; /* RGB values for light mode */
}
html[data-theme='dark'] {
--text-primary: 237 243 255; /* RGB values for dark mode */
}Then use Tailwind's arbitrary value syntax in components: text-[rgb(var(--text-primary)/0.8)]
This eliminates the need for dozens of !important overrides.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/index.css` around lines 119 - 148, The dark-mode CSS block repeats many
!important overrides for selectors like html[data-theme='dark'] .text-black and
.border-black\/35/.bg-white\/30; replace these per-utility overrides by defining
a semantic RGB CSS variable (e.g. --text-primary and --border-primary) inside
html[data-theme='dark'] and :root, then remove the repeated !important rules and
update components to use Tailwind arbitrary colors that reference the variables
(e.g. text-[rgb(var(--text-primary)/0.8)] or
border-[rgb(var(--border-primary)/0.35)]), allowing Tailwind opacity modifiers
to work without specificity hacks.
Summary by CodeRabbit
New Features
Improvements