Context: Why Dual-Mode?
SMO1 brand has two distinct personalities: cat mode (playful, whimsical, emoji-heavy) and professional mode (corporate, neutral). Instead of building separate UIs, we implemented a single-codebase dual-mode system that switches all user-facing terminology at runtime via React Context + localStorage. Default: Cat mode ON. Users can toggle in Settings → Appearance.Architecture Pattern
Three-Layer Stack
-
Context Layer (
meow-web/src/contexts/cat-mode.tsx)- React Context + Provider
- localStorage key:
appearance-cat-mode - Default:
true(cat mode ON) - Exports:
CatModeProvidercomponent +useCatMode()hook - Handles SSR hydration via
mountedstate flag
-
Helper Functions Layer (Component-scoped)
- Mapping objects:
const catLabels = { pawprintz: "73 Pawprintz" }; const proLabels = { pawprintz: "73 Activity" } - Conditional helpers:
const label = isCatMode ? catLabel : proLabel - Pattern: Group related labels into reusable
getLabel()orgetStatLabels()functions
- Mapping objects:
-
Component Layer (Consumer)
- Read
isCatModevia hook:const { isCatMode } = useCatMode() - Apply labels in render: single conditional per label
- Test with
renderWithCatMode()helper wrapping component in provider
- Read
Context Implementation
Terminology Mapping Pattern
Centralize label pairs in component files to minimize conditional noise:Key Discoveries
-
SSR Safety: Components using
useCatMode()render SSR content without the context value. Themountedflag prevents hydration mismatches. Always checkif (!mounted) return nullor return a safe fallback. -
localStorage Default Semantics: When localStorage is empty (first visit), default to
true(cat mode ON). This is the inverse of “feature flags” where true often means “enabled for power users.” Here, cat mode is the default experience. -
Testing Pattern: Wrap test render calls with
CatModeProvider. Build a helper: -
Emoji Placement: In cat mode, emojis are part of the label string, not separate DOM elements. This simplifies rendering and keeps labels in sync:
-
Single-Component Philosophy: Don’t create separate
CatButton+ProButtoncomponents. Use one component with conditional labels. This prevents code divergence and reduces maintenance burden.
Implementation Stats
- Files Created: 1 context file + 3 test files
- Files Modified: 6 component files + 1 provider file
- Tests Added: 18 passing tests (5 context, 5 scoring-chips, 8 cat-status-badge)
- TypeScript Errors: 0
- Build Impact: No bundle size change (context is minimal)
Wins
✅ No code duplication (single component serves both modes) ✅ Trivial to add new terminology pairs (just extend label objects) ✅ localStorage persistence works across sessions ✅ SSR-safe with hydration guards ✅ Testable with isolated context provider wrapper ✅ Cat mode toggle integrated into existing Settings pageGotchas
❌ Forgetting themounted flag → hydration mismatch warnings in dev
❌ localStorage reads happening in component body → React warnings; use useEffect
❌ Conditional text in JSX that’s longer than the conditional logic → use getLabel() helpers
❌ Emojis as separate DOM elements → gets complex with localization; keep in label string
Next Steps
- Implement task 5.6 (pagination terminology) when pagination UI component is created
- A/B test cat vs professional mode on landing page to measure engagement
- Consider adding a “brand persona” config file to centralize all personality text
- Expand pattern to backend API response schemas if cross-environment consistency is needed