feat: Add dark mode toggle to application header#34
Conversation
Adds a sun/moon toggle button that switches between light and dark themes. Persists preference to localStorage and respects system preference on first visit. The dark mode CSS variables were already defined in the project. https://claude.ai/code/session_01VQYSzcnUUBb8cYZvJZC4eG
WalkthroughThe change introduces dark mode toggle functionality to the application. It adds Moon and Sun icon imports, creates a reactive isDark state, implements a toggleDark function to manage theme switching with localStorage persistence, and includes onMounted lifecycle logic to initialize theme from saved preferences or system settings. A toggle button is rendered in the header. Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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: 2
🤖 Fix all issues with AI agents
In `@web/src/App.vue`:
- Around line 8-17: ESLint flags `localStorage` as undefined in toggleDark and
the onMounted block; update references to use a global-qualified accessor (e.g.,
window.localStorage or globalThis.localStorage) to satisfy the linter without
changing behavior. Specifically, inside the toggleDark function (and where
isDark is initialized in the onMounted callback used alongside
document.documentElement.classList.toggle), replace localStorage.getItem/setItem
usages with window.localStorage.getItem/setItem (or globalThis.localStorage) so
the symbols remain unique and lint-clean.
- Around line 46-49: The button element with `@click`="toggleDark" should have its
static and bound attributes ordered before event handlers to satisfy the linter:
move the class and :title attributes to appear before the `@click` attribute on
the <button> (referencing the button that calls the toggleDark method and uses
isDark for the title). Update the attribute order so class and :title come
first, then `@click`, preserving the same values and spacing.
| function toggleDark() { | ||
| isDark.value = !isDark.value | ||
| document.documentElement.classList.toggle('dark', isDark.value) | ||
| localStorage.setItem('theme', isDark.value ? 'dark' : 'light') | ||
| } | ||
|
|
||
| onMounted(() => { | ||
| const saved = localStorage.getItem('theme') | ||
| isDark.value = saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches) | ||
| document.documentElement.classList.toggle('dark', isDark.value) |
There was a problem hiding this comment.
Fix ESLint no-undef for localStorage (CI is failing).
Lint/pipeline errors report localStorage as undefined on Line 11 and Line 15. Use window.localStorage (or globalThis.localStorage) to satisfy the ESLint environment and keep behavior unchanged.
🐛 Proposed fix
function toggleDark() {
isDark.value = !isDark.value
document.documentElement.classList.toggle('dark', isDark.value)
- localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
+ window.localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
}
onMounted(() => {
- const saved = localStorage.getItem('theme')
+ const saved = window.localStorage.getItem('theme')
isDark.value = saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)
document.documentElement.classList.toggle('dark', isDark.value)
})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function toggleDark() { | |
| isDark.value = !isDark.value | |
| document.documentElement.classList.toggle('dark', isDark.value) | |
| localStorage.setItem('theme', isDark.value ? 'dark' : 'light') | |
| } | |
| onMounted(() => { | |
| const saved = localStorage.getItem('theme') | |
| isDark.value = saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches) | |
| document.documentElement.classList.toggle('dark', isDark.value) | |
| function toggleDark() { | |
| isDark.value = !isDark.value | |
| document.documentElement.classList.toggle('dark', isDark.value) | |
| window.localStorage.setItem('theme', isDark.value ? 'dark' : 'light') | |
| } | |
| onMounted(() => { | |
| const saved = window.localStorage.getItem('theme') | |
| isDark.value = saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches) | |
| document.documentElement.classList.toggle('dark', isDark.value) | |
| }) |
🧰 Tools
🪛 GitHub Actions: Frontend CI
[error] 11-11: ESLint: 'localStorage' is not defined. (no-undef)
🪛 GitHub Check: lint-and-test
[failure] 15-15:
'localStorage' is not defined
[failure] 11-11:
'localStorage' is not defined
🤖 Prompt for AI Agents
In `@web/src/App.vue` around lines 8 - 17, ESLint flags `localStorage` as
undefined in toggleDark and the onMounted block; update references to use a
global-qualified accessor (e.g., window.localStorage or globalThis.localStorage)
to satisfy the linter without changing behavior. Specifically, inside the
toggleDark function (and where isDark is initialized in the onMounted callback
used alongside document.documentElement.classList.toggle), replace
localStorage.getItem/setItem usages with window.localStorage.getItem/setItem (or
globalThis.localStorage) so the symbols remain unique and lint-clean.
| <button | ||
| @click="toggleDark" | ||
| class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:text-primary hover:bg-accent" | ||
| :title="isDark ? 'Switch to light mode' : 'Switch to dark mode'" |
There was a problem hiding this comment.
Reorder button attributes to clear lint warnings.
Static analysis reports attribute order warnings. Move class and :title before @click to satisfy the lint rule.
🧹 Proposed fix
- <button
- `@click`="toggleDark"
- class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:text-primary hover:bg-accent"
- :title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
- >
+ <button
+ class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:text-primary hover:bg-accent"
+ :title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
+ `@click`="toggleDark"
+ >🧰 Tools
🤖 Prompt for AI Agents
In `@web/src/App.vue` around lines 46 - 49, The button element with
`@click`="toggleDark" should have its static and bound attributes ordered before
event handlers to satisfy the linter: move the class and :title attributes to
appear before the `@click` attribute on the <button> (referencing the button that
calls the toggleDark method and uses isDark for the title). Update the attribute
order so class and :title come first, then `@click`, preserving the same values
and spacing.
Summary
This PR adds dark mode functionality to the application with a toggle button in the header. The theme preference is persisted to localStorage and respects the user's system color scheme preference on first visit.
Key Changes
MoonandSunicons from lucide-vue-next for the toggle buttonref,onMounted) for state managementImplementation Details
isDarkref tracks the current theme statetoggleDark()function updates the theme, applies the 'dark' class to the document root, and saves the preference to localStoragehttps://claude.ai/code/session_01VQYSzcnUUBb8cYZvJZC4eG
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.