Skip to main content

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

  1. Context Layer (meow-web/src/contexts/cat-mode.tsx)
    • React Context + Provider
    • localStorage key: appearance-cat-mode
    • Default: true (cat mode ON)
    • Exports: CatModeProvider component + useCatMode() hook
    • Handles SSR hydration via mounted state flag
  2. 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() or getStatLabels() functions
  3. Component Layer (Consumer)
    • Read isCatMode via hook: const { isCatMode } = useCatMode()
    • Apply labels in render: single conditional per label
    • Test with renderWithCatMode() helper wrapping component in provider

Context Implementation

// CatModeProvider: wraps app at root (providers.tsx)
export function CatModeProvider({ children }) {
  const [isCatMode, setIsCatMode] = useState<boolean>(true);
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    const stored = localStorage.getItem("appearance-cat-mode");
    setIsCatMode(stored === null ? true : stored === "true");
    setMounted(true);
  }, []);

  const toggle = () => {
    const next = !isCatMode;
    setIsCatMode(next);
    localStorage.setItem("appearance-cat-mode", String(next));
  };

  if (!mounted) return children; // SSR hydration: avoid mismatch

  return (
    <CatModeContext.Provider value={{ isCatMode, toggle, mounted }}>
      {children}
    </CatModeContext.Provider>
  );
}

Terminology Mapping Pattern

Centralize label pairs in component files to minimize conditional noise:
// In ScoringChips component
const catLabels = {
  pawprintz: "Pawprintz",
  treatz: "Treatz",
  niblz: "Niblz",
};
const proLabels = {
  pawprintz: "Activity",
  treatz: "Efficiency",
  niblz: "Compression",
};

const { isCatMode } = useCatMode();
const labels = isCatMode ? catLabels : proLabels;

// In render:
<span>{pawprintz} {labels.pawprintz}</span>

Key Discoveries

  1. SSR Safety: Components using useCatMode() render SSR content without the context value. The mounted flag prevents hydration mismatches. Always check if (!mounted) return null or return a safe fallback.
  2. 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.
  3. Testing Pattern: Wrap test render calls with CatModeProvider. Build a helper:
    function renderWithCatMode(component, catMode = true) {
      localStorage.setItem("appearance-cat-mode", String(catMode));
      return render(<CatModeProvider>{component}</CatModeProvider>);
    }
    
  4. Emoji Placement: In cat mode, emojis are part of the label string, not separate DOM elements. This simplifies rendering and keeps labels in sync:
    const catStatus = "Zoomies ⚡"; // emoji included
    const proStatus = "Active";     // no emoji
    
  5. Single-Component Philosophy: Don’t create separate CatButton + ProButton components. 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 page

Gotchas

❌ Forgetting the mounted 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