diff --git a/.claude/context/frontend-development.md b/.claude/context/frontend-development.md old mode 100644 new mode 100755 index 02e944cab..cde121179 --- a/.claude/context/frontend-development.md +++ b/.claude/context/frontend-development.md @@ -172,12 +172,84 @@ export function useCreateSession(projectName: string) { - `components/frontend/DESIGN_GUIDELINES.md` - Comprehensive patterns - `components/frontend/COMPONENT_PATTERNS.md` - Architecture patterns +- `.claude/patterns/react-state-stability.md` - React rendering & state stability patterns +- `.claude/patterns/react-query-usage.md` - React Query data fetching patterns - `src/components/ui/` - Shadcn UI components - `src/services/queries/` - React Query hooks - `src/services/api/` - API client layer +## Theme Creation Guidelines + +When creating or modifying UI themes (light, dark, custom variants): + +### Visual Distinction Requirements + +**CRITICAL:** New theme variants MUST be visually distinct from existing themes. + +**What "visually distinct" means:** +- Background colors differ by at least 20% lightness (L in OKLCH) +- Primary/accent colors use different hue ranges (not just different shades of the same color) +- At a glance, a user can immediately identify which theme is active + +**Example - What NOT to do:** +```css +/* ❌ BAD: LibreChat theme too similar to light theme */ +.librechat { + --background: oklch(0.98 0 0); /* Nearly white, like light theme */ + --foreground: oklch(0.15 0 0); /* Nearly same as light theme */ + --primary: oklch(0.51 0.21 265); /* Very similar to light theme primary */ +} + +/* Light theme for comparison */ +:root { + --background: oklch(1 0 0); /* White */ + --foreground: oklch(0.145 0 0); /* Dark gray */ + --primary: oklch(0.5 0.22 264); /* Purple-blue */ +} +/* These are nearly indistinguishable! */ +``` + +**Example - What to do:** +```css +/* ✅ GOOD: Solarized theme clearly distinct */ +.solarized-light { + --background: oklch(0.97 0.01 85); /* Warm cream background */ + --foreground: oklch(0.35 0.05 192); /* Cool teal foreground */ + --primary: oklch(0.55 0.15 192); /* Blue-cyan accent */ + /* Clearly different warm/cool palette */ +} + +/* ✅ GOOD: High-contrast theme */ +.high-contrast { + --background: oklch(1 0 0); /* Pure white */ + --foreground: oklch(0 0 0); /* Pure black */ + --primary: oklch(0.45 0.3 240); /* Vibrant blue */ + /* Much stronger contrast than default */ +} +``` + +### Theme Creation Checklist + +Before creating a new theme variant: + +- [ ] Compare background lightness values (L in OKLCH) - minimum 20% difference +- [ ] Check primary color hue - should be different color family (not just darker/lighter) +- [ ] Test with actual UI - can you immediately tell themes apart? +- [ ] Verify contrast ratios meet WCAG AA standards (4.5:1 for text) +- [ ] Check both light and dark variants if creating a full theme + +### Quick Comparison Test + +After creating a theme: +1. Take screenshot of UI in new theme +2. Take screenshot of UI in similar existing theme +3. Place side-by-side +4. If you have to squint or look carefully to tell them apart → **Not distinct enough** + ## Recent Issues & Learnings +- **2026-03-02:** Added React state stability patterns - prevent timestamp re-calculation bugs +- **2026-03-02:** Added theme creation guidelines - ensure visual distinction between theme variants - **2024-11-18:** Migrated all data fetching to React Query - no more manual fetch calls - **2024-11-15:** Enforced Shadcn UI only - removed custom button components - **2024-11-10:** Added breadcrumb pattern for nested pages diff --git a/.claude/patterns/react-state-stability.md b/.claude/patterns/react-state-stability.md new file mode 100644 index 000000000..588b210e5 --- /dev/null +++ b/.claude/patterns/react-state-stability.md @@ -0,0 +1,283 @@ +# React State Stability & Rendering Patterns + +Critical patterns for ensuring stable, predictable values in React components and avoiding common rendering anti-patterns. + +## Core Principle + +**Values displayed in the UI should be stable and not recalculated on every render** unless they explicitly need to be reactive to specific dependencies. + +## Common Anti-Patterns + +### ❌ Anti-Pattern 1: Recalculating Time on Every Render + +**The Bug:** +```tsx +// ❌ BAD: This will show the CURRENT time on every render +function MessageItem({ message }: { message: Message }) { + return ( +
+ {message.content} + {/* WRONG! */} +
+ ) +} +``` + +**Why it's wrong:** +- `new Date()` creates a new timestamp every time the component renders +- If parent re-renders or state changes, all messages show the same "current" time +- By the end of a conversation, all timestamps will appear identical + +**Symptom:** +- Timestamps that update dynamically to show the current time +- All messages eventually showing the same timestamp +- Time values that "drift" as you interact with the page + +**✅ Fix: Use Stable Message Data** +```tsx +// ✅ GOOD: Display the timestamp from the message data +function MessageItem({ message }: { message: Message }) { + return ( +
+ {message.content} + +
+ ) +} +``` + +**✅ Alternative: Memoize Computed Values** +```tsx +// ✅ GOOD: Memoize if you must compute +function MessageItem({ message }: { message: Message }) { + const formattedTime = useMemo( + () => new Date(message.timestamp).toLocaleTimeString(), + [message.timestamp] + ) + + return ( +
+ {message.content} + +
+ ) +} +``` + +### ❌ Anti-Pattern 2: Generating IDs on Every Render + +```tsx +// ❌ BAD: New ID on every render +function FormField() { + const id = `field-${Math.random()}` // WRONG! + return +} + +// ✅ GOOD: Stable ID using useId +function FormField() { + const id = useId() + return +} + +// ✅ GOOD: ID from props or stable source +function FormField({ fieldName }: { fieldName: string }) { + const id = `field-${fieldName}` + return +} +``` + +### ❌ Anti-Pattern 3: Recreating Objects/Arrays on Render + +```tsx +// ❌ BAD: New array reference on every render +function UserList() { + const emptyState = { message: "No users" } // New object every render! + const users = useUsers() + + if (!users.length) return
{emptyState.message}
+} + +// ✅ GOOD: Define outside component or use useMemo +const EMPTY_STATE = { message: "No users" } + +function UserList() { + const users = useUsers() + if (!users.length) return
{EMPTY_STATE.message}
+} + +// ✅ GOOD: Use useMemo for computed objects +function UserList() { + const users = useUsers() + const emptyState = useMemo( + () => ({ message: `No users in ${organizationName}` }), + [organizationName] + ) + + if (!users.length) return
{emptyState.message}
+} +``` + +## Debugging Timestamp/Time-Related Issues + +### Investigation Checklist + +When investigating timestamp bugs, check: + +1. **Where is the time value coming from?** + - [ ] From server/API data (message.timestamp)? + - [ ] From local state (useState)? + - [ ] Computed on every render (new Date())? ← **Most likely culprit** + +2. **Is the value being recalculated?** + - [ ] Is `new Date()` called without arguments? + - [ ] Is `Date.now()` called in the render? + - [ ] Are time formatting functions called with no stable input? + +3. **Is the value memoized?** + - [ ] Is `useMemo` used for expensive computations? + - [ ] Are dependencies specified correctly? + - [ ] Could this value be computed once at data fetch time? + +### Diagnostic Pattern + +```tsx +// Add this to suspect components to track re-renders +function MessageItem({ message }: { message: Message }) { + console.log('MessageItem rendered at:', new Date().toISOString()) + console.log('Message timestamp:', message.timestamp) + + // If these logs show different times but same message.timestamp, + // it means the component is re-rendering but data is stable (good!) + + // If message.timestamp is undefined or changes unexpectedly, + // that's your data problem + + return
...
+} +``` + +### Common Root Causes + +**Timestamps showing current time instead of message time:** +- ✓ Using `new Date()` without an argument in the render +- ✓ Using `Date.now()` in the render +- ✓ Formatting function called on each render without memoization + +**Timestamps changing unexpectedly:** +- ✓ Parent component passing new `Date()` as prop +- ✓ State being updated with current time on each render +- ✓ Time formatting happening in wrong lifecycle stage + +**All timestamps showing the same value:** +- ✓ `new Date()` being called during render (most common!) +- ✓ Single timestamp being reused across all items +- ✓ Timestamp not being included in API response + +## Best Practices + +### 1. Store Timestamps as ISO Strings or Unix Time + +```tsx +// ✅ GOOD: Store as ISO string from server +type Message = { + id: string + content: string + timestamp: string // "2024-01-15T10:30:00Z" +} + +// Format for display +function formatTimestamp(isoString: string): string { + return new Date(isoString).toLocaleTimeString() +} +``` + +### 2. Format Timestamps at the Data Layer (Ideal) + +```tsx +// ✅ BEST: Pre-format in the API response or query +type Message = { + id: string + content: string + timestamp: string + formattedTimestamp: string // Already formatted +} + +// In your API client or React Query select: +const { data } = useQuery({ + queryKey: ['messages'], + queryFn: fetchMessages, + select: (messages) => messages.map(msg => ({ + ...msg, + formattedTimestamp: new Date(msg.timestamp).toLocaleTimeString() + })) +}) +``` + +### 3. Use React.memo for Timestamp Display Components + +```tsx +// ✅ GOOD: Prevent unnecessary re-renders +const Timestamp = React.memo(({ timestamp }: { timestamp: string }) => { + const formatted = useMemo( + () => new Date(timestamp).toLocaleTimeString(), + [timestamp] + ) + + return +}) +``` + +### 4. Freeze Computed Values When Created + +```tsx +// ✅ GOOD: Compute once when message arrives +function useMessages() { + return useQuery({ + queryKey: ['messages'], + queryFn: fetchMessages, + select: (data) => data.messages.map(msg => ({ + ...msg, + // Freeze the display time when message first arrives + displayTime: new Date(msg.timestamp).toLocaleTimeString() + })) + }) +} +``` + +## Quick Reference + +| Scenario | ❌ Anti-Pattern | ✅ Pattern | +|----------|----------------|-----------| +| Display message time | `new Date().toLocaleTimeString()` | `new Date(message.timestamp).toLocaleTimeString()` | +| Display current time | Inline `new Date()` | `useState` + `useEffect` interval | +| Format timestamp | In render without memo | `useMemo` or format in data layer | +| Generate ID | `Math.random()` in render | `useId()` or stable prop | +| Create object | Inline object literal | Constant outside component or `useMemo` | + +## When You See a Timestamp Bug + +**First, verify it's a rendering issue, not a data issue:** + +1. Check the raw data: `console.log(message.timestamp)` +2. If timestamp data is correct but display is wrong → **Rendering issue** +3. If timestamp data is undefined/changing → **Data fetching issue** + +**For rendering issues:** +- Search for `new Date()` without arguments in component +- Search for `Date.now()` in component +- Check if time formatting is inside render without memoization + +**For data issues:** +- Check API response structure +- Verify timestamp is included in GraphQL/REST query +- Check if backend is setting timestamps correctly + +## Summary + +**Golden Rule:** Never compute time-sensitive values (timestamps, IDs, random numbers) directly in the render phase unless you explicitly want them to change on every render. + +**Default approach:** +1. Store stable values from data source (API, props, state) +2. Memoize any transformations with `useMemo` +3. Use stable value generators (`useId`, `useState` with initializer) +4. When in doubt, add a console.log to verify value stability