10 KiB
Settings & Dark Mode Implementation
Status: ✅ Phase 1 Complete, Phase 2 Complete
Last Updated: January 2026
Overview
A comprehensive user settings system with persistent preferences, dark mode support, and customizable list display options. Settings are stored per-user in localStorage and automatically applied across the application.
Architecture
Context Hierarchy
<ConfigProvider> ← Server config (image limits, etc.)
<AuthProvider> ← Authentication state
<SettingsProvider> ← User preferences (NEW)
<App />
</SettingsProvider>
</AuthProvider>
</ConfigProvider>
Key Components
SettingsContext (frontend/src/context/SettingsContext.jsx)
- Manages user preferences with localStorage persistence
- Storage key pattern:
user_preferences_${username} - Automatically applies theme to
document.documentElement - Listens for system theme changes in auto mode
- Provides
updateSettings()andresetSettings()methods
Settings Page (frontend/src/pages/Settings.jsx)
- Tabbed interface: Appearance, List Display, Behavior
- Real-time preview of setting changes
- Reset to defaults functionality
Settings Schema
{
// === Appearance ===
theme: "light" | "dark" | "auto", // Theme mode
compactView: false, // Reduced spacing for denser lists
// === List Display ===
defaultSortMode: "zone", // Default: "zone" | "az" | "za" | "qty-high" | "qty-low"
showRecentlyBought: true, // Toggle recently bought section
recentlyBoughtCount: 10, // Initial items shown (5-50)
recentlyBoughtCollapsed: false, // Start section collapsed
// === Behavior ===
confirmBeforeBuy: true, // Show confirmation modal
autoReloadInterval: 0, // Auto-refresh in minutes (0 = disabled)
hapticFeedback: true, // Vibration on mobile interactions
// === Advanced ===
debugMode: false // Developer tools (future)
}
Dark Mode Implementation
Theme System
Three modes:
- Light: Force light theme
- Dark: Force dark theme
- Auto: Follow system preferences with live updates
CSS Variable Architecture
All colors use CSS custom properties defined in frontend/src/styles/theme.css:
Light Mode (:root):
:root {
--color-text-primary: #212529;
--color-bg-body: #f8f9fa;
--color-bg-surface: #ffffff;
/* ... */
}
Dark Mode ([data-theme="dark"]):
[data-theme="dark"] {
--color-text-primary: #f1f5f9;
--color-bg-body: #0f172a;
--color-bg-surface: #1e293b;
/* ... */
}
Theme Application Logic
// In SettingsContext.jsx
useEffect(() => {
const applyTheme = () => {
let theme = settings.theme;
// Auto mode: check system preference
if (theme === "auto") {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
theme = prefersDark ? "dark" : "light";
}
document.documentElement.setAttribute("data-theme", theme);
};
applyTheme();
// Listen for system theme changes
if (settings.theme === "auto") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", applyTheme);
return () => mediaQuery.removeEventListener("change", applyTheme);
}
}, [settings.theme]);
Integration with Existing Features
GroceryList Integration
Changed:
// Before
const [sortMode, setSortMode] = useState("zone");
const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(10);
// After
const { settings } = useContext(SettingsContext);
const [sortMode, setSortMode] = useState(settings.defaultSortMode);
const [recentlyBoughtDisplayCount, setRecentlyBoughtDisplayCount] = useState(settings.recentlyBoughtCount);
const [recentlyBoughtCollapsed, setRecentlyBoughtCollapsed] = useState(settings.recentlyBoughtCollapsed);
Features:
- Sort mode persists across sessions
- Recently bought section respects visibility setting
- Collapse state controlled by user preference
- Initial display count uses user's preference
File Structure
frontend/src/
├── context/
│ └── SettingsContext.jsx ← Settings state management
├── pages/
│ └── Settings.jsx ← Settings UI
├── styles/
│ ├── theme.css ← Dark mode CSS variables
│ └── pages/
│ └── Settings.css ← Settings page styles
└── App.jsx ← Settings route & provider
Usage Examples
Access Settings in Component
import { useContext } from "react";
import { SettingsContext } from "../context/SettingsContext";
function MyComponent() {
const { settings, updateSettings } = useContext(SettingsContext);
// Read setting
const isDark = settings.theme === "dark";
// Update setting
const toggleTheme = () => {
updateSettings({
theme: settings.theme === "dark" ? "light" : "dark"
});
};
return <button onClick={toggleTheme}>Toggle Theme</button>;
}
Conditional Rendering Based on Settings
{settings.showRecentlyBought && (
<RecentlyBoughtSection />
)}
Using Theme Colors
.my-component {
background: var(--color-bg-surface);
color: var(--color-text-primary);
border: 1px solid var(--color-border-light);
}
localStorage Structure
Key: user_preferences_${username}
Example stored value:
{
"theme": "dark",
"compactView": false,
"defaultSortMode": "zone",
"showRecentlyBought": true,
"recentlyBoughtCount": 20,
"recentlyBoughtCollapsed": false,
"confirmBeforeBuy": true,
"autoReloadInterval": 0,
"hapticFeedback": true,
"debugMode": false
}
Testing Checklist
Settings Page
- All three tabs accessible
- Theme toggle works (light/dark/auto)
- Auto mode follows system preference
- Settings persist after logout/login
- Reset button restores defaults
Dark Mode
- All pages render correctly in dark mode
- Modals readable in dark mode
- Forms and inputs visible in dark mode
- Navigation and buttons styled correctly
- Images and borders contrast properly
GroceryList Integration
- Default sort mode applied on load
- Recently bought visibility respected
- Collapse state persists during session
- Display count uses user preference
Future Enhancements (Not Implemented)
Phase 3: Advanced Preferences
- Compact View: Reduced padding/font sizes for power users
- Confirm Before Buy: Toggle for confirmation modal
- Auto-reload: Refresh list every X minutes for shared lists
Phase 4: Account Management
- Change Password: Security feature (needs backend endpoint)
- Display Name: Friendly name separate from username
Phase 5: Data Management
- Export List: Download as CSV/JSON
- Clear History: Remove recently bought items
- Import Items: Bulk add from file
Phase 6: Accessibility
- Font Size: Adjustable text sizing
- High Contrast Mode: Increased contrast for visibility
- Reduce Motion: Disable animations
API Endpoints
None required - all settings are client-side only.
Future backend endpoints may include:
PATCH /api/users/me- Update user profile (password, display name)GET /api/list/export- Export grocery list data
Browser Compatibility
Theme Detection
- Chrome/Edge: ✅ Full support
- Firefox: ✅ Full support
- Safari: ✅ Full support (iOS 12.2+)
- Mobile browsers: ✅ Full support
localStorage
- All modern browsers: ✅ Supported
- Fallback: Settings work but don't persist (rare)
Troubleshooting
Settings Don't Persist
Issue: Settings reset after logout
Cause: Settings tied to username
Solution: Working as designed - each user has separate preferences
Dark Mode Not Applied
Issue: Page stays light after selecting dark
Cause: Missing data-theme attribute
Solution: Check SettingsContext is wrapped around App
System Theme Not Detected
Issue: Auto mode doesn't work
Cause: Browser doesn't support prefers-color-scheme
Solution: Fallback to light mode (handled automatically)
Development Notes
Adding New Settings
- Update DEFAULT_SETTINGS in SettingsContext.jsx:
const DEFAULT_SETTINGS = {
// ...existing settings
myNewSetting: defaultValue,
};
- Add UI in Settings.jsx:
<div className="settings-group">
<label className="settings-label">
<input
type="checkbox"
checked={settings.myNewSetting}
onChange={() => handleToggle("myNewSetting")}
/>
<span>My New Setting</span>
</label>
<p className="settings-description">Description here</p>
</div>
- Use in components:
const { settings } = useContext(SettingsContext);
if (settings.myNewSetting) {
// Do something
}
Adding Theme Colors
- Define in both light (
:root) and dark ([data-theme="dark"]) modes - Use descriptive semantic names:
--color-purpose-variant - Always provide fallbacks for older code
Performance Considerations
- Settings load once per user session
- Theme changes apply instantly (no page reload)
- localStorage writes are debounced by React state updates
- No network requests for settings (all client-side)
Accessibility
- ✅ Keyboard navigation works in Settings page
- ✅ Theme buttons have clear active states
- ✅ Range sliders show current values
- ✅ Color contrast meets WCAG AA in both themes
- ⚠️ Screen reader announcements for theme changes (future enhancement)
Migration Notes
Upgrading from older versions:
- Old settings are preserved (merged with defaults)
- Missing settings use default values
- Invalid values are reset to defaults
- No migration script needed - handled automatically
Related Documentation
- Code Cleanup Guide - Code organization patterns
- Component Structure - Component architecture
- Theme Usage Examples - CSS variable usage
Implementation Status: ✅ Complete
Phase 1 (Foundation): ✅ Complete
Phase 2 (Dark Mode): ✅ Complete
Phase 3 (List Preferences): ✅ Complete
Phase 4+ (Future): ⏳ Planned