Skip to content

feat(zustand): implement dark/light theme toggle with zustand - #7

Open
vidyashreebv wants to merge 8 commits into
refactor/apply-code-review-feedbackfrom
feature/zustand-theme-management
Open

vidyashreebv wants to merge 8 commits into
refactor/apply-code-review-feedbackfrom
feature/zustand-theme-management

Conversation

@vidyashreebv

Copy link
Copy Markdown
Collaborator

What does this PR do?

  • Implements Zustand for lightweight global state management
  • Adds dark/light theme toggle functionality using Zustand store
  • Creates useThemeStore with toggleTheme and setTheme actions
  • Applies dark mode styling to all components using Tailwind v4 dark: variants
  • Adds 8 comprehensive test cases for theme functionality

What steps does your reviewer have to take to test this PR manually?

  1. Pull the feature/zustand-theme-management branch and run npm install
  2. Run npm run dev to start the development server
  3. Click the theme toggle button (moon/sun icon) in top-right to switch between light and dark modes
  4. Verify all components update colors correctly in both themes (background, cards, text, inputs, buttons)
  5. Test counter functionality (increment, decrement, reset, settings) works in both themes
  6. Refresh the page and verify theme preference persists in localStorage
  7. Test negative values and custom step values in both themes
  8. Run npm test -- --run to verify all 35 tests pass (27 existing + 8 new theme tests)

Screenshots

Screenshot 2026-01-09 at 3 33 51 PM Screenshot 2026-01-09 at 3 34 04 PM

Pull Request standards checklist - Please check off

  • This Branch will be carrying one single responsibility - feature/bugfix/style/refactor...
  • I have followed conventional commit messages and descriptive Branch naming.
  • My PR has descriptive folder/file names that I have worked on.

Testing checklist - Please check off

  • I have performed manual testing on my local to validate all changes.

Definition of Done - Please check off

  • My code is well tested and I have confidence my code works as I expect in a variety of situations.
  • I have lint and format code is enabled when the file is saved and I have fixed all errors highlighted by lint.
  • I have deleted all non-descriptive comments and dead code from the files I've touched.
  • I have rebased my branch with the base branch I want to merge into and all the commits in this PR are my own.

- Create Zustand store for theme state management
- Add toggleTheme action to switch between light and dark modes
- Add setTheme action for direct theme setting
- Implement localStorage persistence for theme preference
- Add SSR safety with globalThis.window checks
- Create ThemeToggle atom component with sun/moon icons
- Integrate with Zustand theme store using useThemeStore hook
- Add comprehensive test suite with 8 test cases
- Test theme toggling, initial state, and accessibility features
- Include Tailwind dark mode classes for styling
- Add useThemeStore hook to manage theme state
- Implement useEffect to apply dark class to document root
- Add ThemeToggle component to top-right corner
- Add dark mode Tailwind classes to all UI elements
- Update background gradients for dark mode support
- Style settings panel, inputs, and labels for theme switching
- Add zustand@^5.0.2 to project dependencies
- Update package-lock.json with zustand and its dependencies
- Remove tailwind.config.js for Tailwind v4 CSS-based configuration
- Add @variant dark directive to index.css for dark mode
- Add stylelint-disable comment for unknown @variant directive
- Enable prefers-color-scheme dark mode detection
- Remove incorrect @variant directive syntax
- Use class-based dark mode with Tailwind v4
- Dark mode works automatically when 'dark' class is on document root
- Add @theme with color-scheme configuration to index.css
- Add colorScheme style property to document root
- Ensures proper dark mode activation in Tailwind v4
- Add @variant dark directive to enable dark: prefix
- Use selector pattern for class-based dark mode
- Remove debug console.log statements
- Dark mode now working correctly with theme toggle
@mergemitra

mergemitra Bot commented Jan 9, 2026

Copy link
Copy Markdown

Change Summary

This PR introduces global theme state using Zustand and adds a light/dark theme toggle UI.
It persists the selected theme to localStorage and applies the dark class/color-scheme at the document root.
Tailwind is updated to enable dark: variants via CSS, and a new ThemeToggle test suite is added.

Note: The PR description mentions dark styling across all components; the diff primarily updates App (and adds the toggle), without showing changes inside other existing components (e.g., Counter).

File Changes

File Summary
package.json Adds Zustand dependency for global theme state management.
src/App.jsx Wires theme store into app, applies root dark class, and adds ThemeToggle UI.
src/components/atoms/ThemeToggle/ThemeToggle.jsx Adds a theme toggle button component with light/dark icons and accessibility labels.
src/components/atoms/ThemeToggle/ThemeToggle.test.jsx Adds tests covering rendering, labels, and theme toggling behavior via Zustand store.
src/index.css Enables Tailwind v4 dark variant via @variant configuration in CSS.
src/store/useThemeStore.js Adds Zustand theme store with mode, toggleTheme, setTheme, and localStorage persistence.
tailwind.config.js Removes Tailwind config file (no longer used/required for current setup).

@mergemitra

mergemitra Bot commented Jan 9, 2026

Copy link
Copy Markdown

PR Scorecard

Score

Communication Quality Code Correctness & Design Quality Test Quality & Coverage Code Readability & Maintainability
Scoring Methodology

Communication Score

The overall communication score is a weighted average:

Dimension Weight Evaluates
Description Quality 60% Title format (conventional commits) + Description clarity (what changed & why)
PR Size & Scope 25% Appropriate sizing, scope cohesion, and justification for size
Commit Messages 15% Conventional commits format, atomic & descriptive changes

Formula: (Description x 0.6) + (PR Size x 0.25) + (Commits x 0.15)

Scoring Framework

The scorecard evaluates code using 3 key reviewer questions:

Reviewer Question Category Subcategories
Is this the right solution, implemented the right way? Code Correctness & Design Quality Correctness, Robustness, Best Practices, Architecture
Would this catch bugs if the code broke tomorrow? Test Quality & Coverage Coverage, Test Design
Can someone new understand and safely modify this in 6 months? Code Readability & Maintainability Clarity, Structure, Consistency

Each category score is the average of its subcategories.

Score Bands: 90-100 Excellent | 75-89 Good | 60-74 Adequate | 40-59 Needs Work | 0-39 Critical

PR Communication Notes

Description Quality

  • ✅ Title follows conventional commits format with clear scope (feat(zustand): ...)
  • ✅ Description includes clear what/why, testing steps, screenshots, and checklist completion

PR Size & Scope

  • ✅ Small, well-scoped change (224 additions, 8 files) with tests included; size is appropriate for a feature+tests

Commit Messages

  • ✅ All commits follow conventional commits format and are descriptive (types/scopes present)
  • ❌ Description claims 'Applies dark mode styling to all components' but changes modify App, ThemeToggle, index.css and tailwind config only — update wording to reflect actual scope

Notes

Code Correctness & Design Quality

  • ❌ 🟠 setTheme(mode) and getInitialTheme() accept/return any string from localStorage without validation, so an unexpected value can put the app in an undefined theme state at src/store/useThemeStore.js
  • ❌ 🔴 Direct localStorage reads/writes can throw (e.g., blocked storage, privacy mode, quota) and would crash render/import paths at src/store/useThemeStore.js
  • ❌ 🟠 Environment check only guards on window but not on localStorage existence; environments with a window shim but no localStorage will still throw at src/store/useThemeStore.js
  • 💭 [nitpick] Add type="button" to prevent accidental form submission if ThemeToggle is ever rendered inside a <form> at src/components/atoms/ThemeToggle/ThemeToggle.jsx
  • ❌ 🟠 ThemeToggle subscribes to the entire Zustand store (useThemeStore() without a selector), causing unnecessary re-renders as the store grows at src/components/atoms/ThemeToggle/ThemeToggle.jsx
  • 💭 [nitpick] Persist key 'theme' is a magic string; extract to a named constant to avoid drift across reads/writes at src/store/useThemeStore.js
  • ❌ 🟠 Theme side effects (toggling .dark on <html> and setting colorScheme) live in App, creating hidden coupling (store updates won’t affect DOM if App isn’t mounted, and reuse outside App is harder) at src/App.jsx

Test Quality & Coverage

  • ❌ 🟠 No tests assert persistence behavior (writes to localStorage on toggle/set, and initial mode loading from localStorage) at src/store/useThemeStore.js
  • ❌ 🟠 No tests verify the DOM side effects (documentElement.classList / colorScheme) driven by theme changes at src/App.jsx
  • ❌ 🟠 Component tests largely assert Zustand internal state (useThemeStore.getState()) instead of user-visible behavior (e.g., aria-label/icon changes after click), making tests more brittle and less representative at src/components/atoms/ThemeToggle/ThemeToggle.test.jsx
  • ❌ 🟠 Tests reset store mode but don’t reset localStorage between tests; once persistence tests are added, this will lead to order-dependent/flaky behavior at src/components/atoms/ThemeToggle/ThemeToggle.test.jsx

Code Readability & Maintainability

  • 💭 [nitpick] State name key is non-descriptive for its purpose (forcing Counter remount); consider counterInstanceKey (or similar) at src/App.jsx
  • ❌ 🟡 Inconsistent formatting/indentation compared to the rest of the codebase (e.g., 4-space indentation in new components/tests vs 2-space elsewhere) reduces readability and increases diff noise at src/components/atoms/ThemeToggle/ThemeToggle.jsx and src/components/atoms/ThemeToggle/ThemeToggle.test.jsx

Comment on lines +11 to +28
export const useThemeStore = create((set) => ({
mode: getInitialTheme(),
toggleTheme: () =>
set((state) => {
const newMode = state.mode === 'light' ? 'dark' : 'light'
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', newMode)
}
return { mode: newMode }
}),
setTheme: (mode) =>
set(() => {
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', mode)
}
return { mode }
}),
}))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Coverage: Add persistence tests for the theme store

There are no tests asserting that theme changes are persisted (setItem calls) and that initial mode is loaded from localStorage.

Why this matters: Persistence is core behavior here; regressions will only show up manually (refresh flows).

Suggested fix:

// src/store/useThemeStore.test.js
import { describe, it, expect, vi, beforeEach } from 'vitest'

beforeEach(() => {
  localStorage.clear()
})

describe('useThemeStore persistence', () => {
  it('writes to localStorage when toggling', async () => {
    vi.resetModules()
    const setItemSpy = vi.spyOn(Storage.prototype, 'setItem')

    const { useThemeStore } = await import('./useThemeStore')
    useThemeStore.setState({ mode: 'light' })

    useThemeStore.getState().toggleTheme()
    expect(setItemSpy).toHaveBeenCalledWith('theme', 'dark')
  })

  it('initializes from localStorage when present', async () => {
    localStorage.setItem('theme', 'dark')

    vi.resetModules()
    const { useThemeStore } = await import('./useThemeStore')

    expect(useThemeStore.getState().mode).toBe('dark')
  })
})

Comment on lines +8 to +11
beforeEach(() => {
// Reset store to light mode before each test
useThemeStore.setState({ mode: 'light' })
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Test Design: Reset localStorage between tests (avoid order-dependence)

Tests reset the store, but don’t reset localStorage. Once persistence coverage is added, this will create flaky/order-dependent tests.

Why this matters: Local runs might pass while CI fails depending on test order.

Suggested fix:

beforeEach(() => {
  localStorage.clear()
  useThemeStore.setState({ mode: 'light' })
})

<button
onClick={handleClickToggleTheme}
aria-label={`Switch to ${isDarkMode ? 'light' : 'dark'} mode`}
className="p-3 rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200 dark:border-gray-700"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Robustness: Prevent accidental form submission

If ThemeToggle is ever used inside a <form>, the default button type will submit the form.

Suggested fix:

<button
  type="button"
  onClick={handleClickToggleTheme}
  aria-label={`Switch to ${isDarkMode ? 'light' : 'dark'} mode`}
  className="..."
>

Comment on lines +21 to +27
setTheme: (mode) =>
set(() => {
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', mode)
}
return { mode }
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Correctness: Validate theme mode values

setTheme(mode) will accept any string (and getInitialTheme() can return any string from storage), which can put the app into an unsupported theme state.

Why this matters: Once mode is something unexpected, UI + DOM side effects can drift into a partially-themed state and become hard to reason about.

Suggested fix:

const THEME_STORAGE_KEY = 'theme'
const VALID_MODES = new Set(['light', 'dark'])

const normalizeMode = (value) => (VALID_MODES.has(value) ? value : 'light')

export const useThemeStore = create((set) => ({
  mode: normalizeMode(getInitialTheme()),
  toggleTheme: () =>
    set((state) => {
      const newMode = state.mode === 'light' ? 'dark' : 'light'
      safeSetItem(THEME_STORAGE_KEY, newMode)
      return { mode: newMode }
    }),
  setTheme: (mode) =>
    set(() => {
      const normalizedMode = normalizeMode(mode)
      safeSetItem(THEME_STORAGE_KEY, normalizedMode)
      return { mode: normalizedMode }
    }),
}))


const getInitialTheme = () => {
if (globalThis.window !== undefined) {
const savedTheme = globalThis.localStorage.getItem('theme')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Best Practices: Extract magic storage key

'theme' is repeated across reads/writes.

Why this matters: A single constant prevents drift if the key ever changes.

Suggested fix:

const THEME_STORAGE_KEY = 'theme'

// then use THEME_STORAGE_KEY everywhere
globalThis.localStorage.setItem(THEME_STORAGE_KEY, newMode)

Comment on lines +16 to +18
if (globalThis.window !== undefined) {
globalThis.localStorage.setItem('theme', newMode)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Robustness: Guard for localStorage availability (not just window)

The current environment check only gates on window, but some environments can have a window shim without localStorage (or a throwing localStorage getter).

Why this matters: You can still throw in non-browser runtimes/tests or constrained browser contexts.

Suggested fix:

const getSafeLocalStorage = () => {
  try {
    return typeof globalThis.window !== 'undefined' && globalThis.window?.localStorage
      ? globalThis.window.localStorage
      : null
  } catch {
    return null
  }
}

const safeGetItem = (key) => {
  const storage = getSafeLocalStorage()
  if (!storage) return null
  try {
    return storage.getItem(key)
  } catch {
    return null
  }
}

const safeSetItem = (key, value) => {
  const storage = getSafeLocalStorage()
  if (!storage) return
  try {
    storage.setItem(key, value)
  } catch {
    // ignore
  }
}

Comment thread src/App.jsx
Comment on lines +12 to +22
// Apply theme to document on mount and when theme changes
useEffect(() => {
const root = document.documentElement
if (mode === 'dark') {
root.classList.add('dark')
root.style.colorScheme = 'dark'
} else {
root.classList.remove('dark')
root.style.colorScheme = 'light'
}
}, [mode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Coverage: Add tests for DOM side effects when theme changes

There are no tests verifying document.documentElement.classList / colorScheme changes when the theme updates.

Why this matters: The UI can look correct in component-level tests while the actual DOM-level dark mode toggle silently regresses.

Suggested fix:

// src/App.test.jsx
import { beforeEach, describe, expect, it } from 'vitest'
import { render } from '@testing-library/react'
import App from './App'
import { useThemeStore } from './store/useThemeStore'

beforeEach(() => {
  useThemeStore.setState({ mode: 'light' })
  document.documentElement.classList.remove('dark')
  document.documentElement.style.colorScheme = ''
})

describe('theme DOM side effects', () => {
  it('applies .dark class and dark color-scheme when mode becomes dark', () => {
    render(<App />)
    useThemeStore.setState({ mode: 'dark' })

    expect(document.documentElement).toHaveClass('dark')
    expect(document.documentElement.style.colorScheme).toBe('dark')
  })
})

Comment on lines +3 to +9
const getInitialTheme = () => {
if (globalThis.window !== undefined) {
const savedTheme = globalThis.localStorage.getItem('theme')
return savedTheme || 'light'
}
return 'light'
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🔴 Critical

Robustness: localStorage access can throw during import/render

getInitialTheme() reads from localStorage and runs as part of the store initialization (mode: getInitialTheme()), so any localStorage exception (privacy mode, blocked storage, quota) can crash the module import / initial render.

Why this matters: A thrown error here prevents the app from booting at all.

Suggested fix:

const safeGetItem = (key) => {
  try {
    return globalThis.localStorage?.getItem(key) ?? null
  } catch {
    return null
  }
}

const safeSetItem = (key, value) => {
  try {
    globalThis.localStorage?.setItem(key, value)
  } catch {
    // ignore persistence failures; app should still work
  }
}

const getInitialTheme = () => safeGetItem(THEME_STORAGE_KEY) || 'light'

Comment thread src/App.jsx
Comment on lines +12 to +22
// Apply theme to document on mount and when theme changes
useEffect(() => {
const root = document.documentElement
if (mode === 'dark') {
root.classList.add('dark')
root.style.colorScheme = 'dark'
} else {
root.classList.remove('dark')
root.style.colorScheme = 'light'
}
}, [mode])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Architecture: Theme DOM side effects are coupled to App

The .dark class and colorScheme mutations live inside App, which makes theme application dependent on App being mounted and harder to reuse (e.g., if you later mount different roots or isolate parts in tests/storybook).

Why this matters: Centralizing the DOM sync reduces hidden coupling and makes theme behavior consistent wherever the store is used.

Recommended approach: extract a dedicated hook (or provider) responsible for syncing store → DOM.

// src/theme/useApplyThemeToDocument.js
import { useEffect } from 'react'
import { useThemeStore } from '../store/useThemeStore'

export function useApplyThemeToDocument() {
  const mode = useThemeStore((s) => s.mode)

  useEffect(() => {
    const root = document.documentElement
    const isDark = mode === 'dark'

    root.classList.toggle('dark', isDark)
    root.style.colorScheme = isDark ? 'dark' : 'light'
  }, [mode])
}

// App.jsx
import { useApplyThemeToDocument } from './theme/useApplyThemeToDocument'

function App() {
  useApplyThemeToDocument()
  // ...
}

Comment on lines +32 to +40
it('toggles theme from light to dark when clicked', async () => {
render(<ThemeToggle />)
const user = userEvent.setup()

const button = screen.getByRole('button')
await user.click(button)

expect(useThemeStore.getState().mode).toBe('dark')
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Test Design: Prefer user-visible assertions over store internals

Several tests assert useThemeStore.getState().mode after clicks, which couples component tests to Zustand internals rather than verifying what the user sees (aria-label/icon).

Why this matters: These tests become brittle if the state implementation changes while the UI behavior stays correct.

Suggested fix:

it('switches aria-label from dark->light after click', async () => {
  render(<ThemeToggle />)
  const user = userEvent.setup()

  const button = screen.getByRole('button', { name: 'Switch to dark mode' })
  await user.click(button)

  expect(
    screen.getByRole('button', { name: 'Switch to light mode' })
  ).toBeInTheDocument()
})

Comment thread src/App.jsx
const mode = useThemeStore((state) => state.mode)
const [initialValue, setInitialValue] = useState(10)
const [step, setStep] = useState(5)
const [key, setKey] = useState(0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 💭 Minor [nitpick]

Clarity: Rename key to reflect intent

key is used specifically to force a Counter remount; a more descriptive name will make that purpose obvious.

Suggested fix:

const [counterInstanceKey, setCounterInstanceKey] = useState(0)

const handleClickApply = () => {
  setCounterInstanceKey((prev) => prev + 1)
}

<Counter key={counterInstanceKey} initialValue={initialValue} step={step} />

import { useThemeStore } from '../../../store/useThemeStore'

export const ThemeToggle = () => {
const { mode, toggleTheme } = useThemeStore()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟠 Major

Best Practices: Avoid subscribing to the entire Zustand store

useThemeStore() without a selector subscribes the component to all store changes, which will cause unnecessary re-renders as the store grows.

Why this matters: It’s easy for small stores to grow over time; this creates avoidable render churn.

Suggested fix:

export const ThemeToggle = () => {
  const mode = useThemeStore((state) => state.mode)
  const toggleTheme = useThemeStore((state) => state.toggleTheme)
  // ...
}

Comment on lines +1 to +51
import { useThemeStore } from '../../../store/useThemeStore'

export const ThemeToggle = () => {
const { mode, toggleTheme } = useThemeStore()

const handleClickToggleTheme = () => {
toggleTheme()
}

const isDarkMode = mode === 'dark'

return (
<button
onClick={handleClickToggleTheme}
aria-label={`Switch to ${isDarkMode ? 'light' : 'dark'} mode`}
className="p-3 rounded-lg bg-white dark:bg-gray-800 shadow-md hover:shadow-lg transition-all duration-200 border border-gray-200 dark:border-gray-700"
>
{isDarkMode ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-6 h-6 text-yellow-500"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-6 h-6 text-gray-700"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
)}
</button>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: 🟡 Moderate

Consistency: Formatting/indentation differs from the rest of the codebase

This file (and ThemeToggle.test.jsx) uses 4-space indentation while most other files use 2-space indentation.

Why this matters: Inconsistent formatting increases diff noise and makes it harder to scan PRs quickly.

Suggested fix:

export const ThemeToggle = () => {
  const mode = useThemeStore((state) => state.mode)
  const toggleTheme = useThemeStore((state) => state.toggleTheme)

  const handleClickToggleTheme = () => {
    toggleTheme()
  }

  const isDarkMode = mode === 'dark'

  return (
    <button type="button" onClick={handleClickToggleTheme} aria-label={`Switch to ${isDarkMode ? 'light' : 'dark'} mode`}>
      {/* ... */}
    </button>
  )
}

@mergemitra

mergemitra Bot commented Jan 9, 2026

Copy link
Copy Markdown

PR Analysis

Focus Areas for Architect Review

  1. Theme state + persistence contract: Decide the canonical source of truth (store vs system preference), how strict the allowed values are, and what the fallback behavior should be when persistence is unavailable.
  2. Theme application boundary: Align on where DOM-side theme syncing should live (App vs dedicated hook/provider) to support Storybook/tests/multi-root rendering consistently.
  3. State management direction: Since Zustand is now introduced, confirm conventions for selectors, store structure, and future growth (to avoid “small store becomes global dumping ground”).

Recommendation

Action: REQUEST CHANGES

Quick Wins:

  • Add safe localStorage wrappers (try/catch) and use them for initial load + persistence.
  • Update ThemeToggle to use Zustand selectors instead of subscribing to the whole store.
  • In tests, clear localStorage in beforeEach and assert user-visible changes (e.g., updated aria-label) after toggling.
Author Insights

PR Type: feat

Missing Skills

  • Robustness: Local storage errors can crash the app.
  • Test Design: Tests check store state, not user behavior.
  • Test Coverage: Persistence and DOM theme changes are untested.
  • Code Consistency: New code style differs from existing style.

Strengths

  • Description Quality: Clear description, steps, and screenshots help reviewers.
  • PR Size: PR is well scoped and easy to review.
  • Commit Messages: Commits are clear and follow a standard format.
  • Code Structure: Changes are grouped in logical new components.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants