From c55b370f28d6633c4cdaaabc4ce83d5b8989f76b Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:24:55 +0200 Subject: [PATCH 1/8] docs: Add UX improvements to match Gmail/Outlook premium standards --- UX_IMPROVEMENTS.md | 490 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 490 insertions(+) create mode 100644 UX_IMPROVEMENTS.md diff --git a/UX_IMPROVEMENTS.md b/UX_IMPROVEMENTS.md new file mode 100644 index 0000000..d6bc5f4 --- /dev/null +++ b/UX_IMPROVEMENTS.md @@ -0,0 +1,490 @@ +# Flow Mail - Premium UX Improvements (Gmail/Outlook Standard) + +## Visual Design Enhancements + +### 1. **Modern Typography System** 📝 +```css +/* Add to global styles */ +:root { + --font-family-display: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; + --font-family-mono: 'Monaco', 'Courier New', monospace; + + --text-xs: 12px; + --text-sm: 13px; + --text-base: 15px; + --text-lg: 17px; + --text-xl: 20px; + --text-2xl: 24px; + + --line-height-tight: 1.2; + --line-height-normal: 1.5; + --line-height-relaxed: 1.75; + + --letter-spacing-tight: -0.02em; + --letter-spacing-normal: 0; + --letter-spacing-wide: 0.02em; +} +``` + +**Changes:** +- Better font sizes for readability +- Consistent line heights +- Professional letter spacing + +--- + +### 2. **Premium Color Palette** 🎨 +```css +:root { + /* Primary Colors */ + --color-primary: #1a73e8; + --color-primary-hover: #1765cc; + --color-primary-focus: #1e40af; + + /* Semantic Colors */ + --color-success: #0d652d; + --color-warning: #b3930f; + --color-error: #d93025; + --color-info: #1a73e8; + + /* Neutral Grays (like Gmail) */ + --color-gray-50: #f8f9fa; + --color-gray-100: #f3f3f3; + --color-gray-200: #e8eaed; + --color-gray-300: #dadce0; + --color-gray-400: #bdc1c6; + --color-gray-500: #9aa0a6; + --color-gray-600: #80868b; + --color-gray-700: #5f6368; + --color-gray-800: #3c4043; + --color-gray-900: #202124; + + /* Background Colors */ + --color-bg: #ffffff; + --color-bg-hover: #f8f9fa; + --color-bg-selected: #fce8e6; + + /* Text Colors */ + --color-text-primary: #202124; + --color-text-secondary: #5f6368; + --color-text-disabled: #9aa0a6; +} + +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #121212; + --color-bg-hover: #262626; + --color-bg-selected: #421f1f; + --color-text-primary: #e8eaed; + --color-text-secondary: #9aa0a6; + } +} +``` + +--- + +### 3. **Spacing System** 📐 +```css +:root { + --space-0: 0; + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-7: 28px; + --space-8: 32px; + --space-12: 48px; + --space-16: 64px; +} +``` + +--- + +### 4. **Elevation & Shadows** ✨ +```css +:root { + --shadow-sm: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15); + --shadow-md: 0 1px 3px 0 rgba(60, 64, 67, 0.3), 0 4px 8px 3px rgba(60, 64, 67, 0.15); + --shadow-lg: 0 4px 6px 3px rgba(60, 64, 67, 0.15), 0 12px 16px 4px rgba(60, 64, 67, 0.1); + --shadow-xl: 0 8px 12px 6px rgba(60, 64, 67, 0.15), 0 20px 25px 6px rgba(60, 64, 67, 0.1); + + --radius-sm: 2px; + --radius-md: 4px; + --radius-lg: 8px; + --radius-full: 9999px; +} +``` + +--- + +### 5. **Smooth Animations** 🎬 +```css +:root { + --transition-fast: 150ms cubic-bezier(0.4, 0, 0.6, 1); + --transition-base: 200ms cubic-bezier(0.4, 0, 0.6, 1); + --transition-slow: 300ms cubic-bezier(0.4, 0, 0.6, 1); +} + +/* Button hover effect */ +.button { + transition: all var(--transition-fast); +} + +.button:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +/* Message list smooth scroll */ +.message-list { + scroll-behavior: smooth; +} + +/* Fade in new messages */ +@keyframes fadeInMessage { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message-list .new-message { + animation: fadeInMessage var(--transition-base); +} +``` + +--- + +## Interaction Improvements + +### 1. **Keyboard Shortcuts** ⌨️ +```typescript +// Add to FlowConsole.tsx +const keyboardShortcuts = { + '/': 'Search', + 'j': 'Next message', + 'k': 'Previous message', + 'e': 'Archive', + '#': 'Delete', + 'c': 'Compose', + 'r': 'Reply', + 'escape': 'Close modal', + '?': 'Show help', +}; + +// Implement in useEffect +useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Only activate when not typing in input + if ((e.target as HTMLElement).tagName === 'INPUT') return; + + switch (e.key) { + case 'j': + openThreadByOffset(1); // Next + break; + case 'k': + openThreadByOffset(-1); // Previous + break; + case 'c': + setComposeOpen(true); + break; + case 'r': + selectedThread && replyToMessage(selectedThread.messages[0]); + break; + case 'e': + selectedThread && archiveOpenThread(); + break; + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); +}, [selectedThread, selectedThreadIndex]); +``` + +--- + +### 2. **Optimistic Updates** ⚡ +```typescript +// Update UI immediately, sync with server +const toggleStarred = (messageId: string) => { + // 1. Optimistically update UI + setMessages(current => + current.map(item => + item.id === messageId ? { ...item, starred: !item.starred } : item + ) + ); + + // 2. Sync with server + fetch(apiUrl(`/flow/messages/${messageId}/star`), { + body: JSON.stringify({ starred: !message?.starred }), + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + }) + .then(() => { + // Success - no need to update + showStatus('Message starred' ); + }) + .catch(() => { + // Revert on failure + setMessages(current => + current.map(item => + item.id === messageId + ? { ...item, starred: message?.starred } + : item + ) + ); + showError('Failed to update message'); + }); +}; +``` + +--- + +### 3. **Skeleton Loaders** 💀 +```typescript +// Create SkeletonLoader component +export function SkeletonLoader({ count = 5 }: { count?: number }) { + return ( +
+ {Array.from({ length: count }).map((_, i) => ( +
+ ))} +
+ ); +} + +// Use instead of "Loading..." text +{isLoadingMessages ? ( + +) : ( + // Message list +)} +``` + +--- + +### 4. **Toast Notifications** 📢 +```typescript +// Add Toast system +interface Toast { + id: string; + message: string; + type: 'success' | 'error' | 'info'; + duration: number; +} + +const [toasts, setToasts] = useState([]); + +function showToast(message: string, type: Toast['type'] = 'info', duration = 3000) { + const id = crypto.randomUUID(); + setToasts(current => [...current, { id, message, type, duration }]); + + setTimeout(() => { + setToasts(current => current.filter(t => t.id !== id)); + }, duration); +} + +// Render toasts +
+ {toasts.map(toast => ( +
+ {toast.message} +
+ ))} +
+``` + +--- + +### 5. **Smart Search Suggestions** 🔍 +```typescript +// Add search suggestions like Gmail +const searchSuggestions = [ + { icon: '📌', label: 'Starred', query: 'is:starred' }, + { icon: '✉️', label: 'Unread', query: 'is:unread' }, + { icon: '📎', label: 'Has attachment', query: 'has:attachment' }, + { icon: '📅', label: 'From today', query: 'after:today' }, +]; + +// Show suggestions when search is active but empty +{query.length > 0 && visibleThreads.length === 0 && ( +
+

No results found

+

Try different keywords

+
+)} +``` + +--- + +## Accessibility Enhancements + +### 1. **Focus Management** +```css +/* Better focus visibility */ +:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +button:focus-visible { + box-shadow: 0 0 0 3px rgba(26, 115, 232, 0.2); +} +``` + +### 2. **Screen Reader Labels** +```tsx + + + + Move this conversation to your archive + +``` + +### 3. **Color Contrast** ✓ +All text meets WCAG AA standards: +- Normal text: 4.5:1 contrast ratio +- Large text: 3:1 contrast ratio + +--- + +## Mobile & Responsive Design + +### 1. **Responsive Layout** +```css +/* Mobile-first approach */ +.message-list { + display: grid; + grid-template-columns: 1fr; +} + +/* Tablet */ +@media (min-width: 768px) { + .workspace { + grid-template-columns: 250px 1fr; + } +} + +/* Desktop */ +@media (min-width: 1024px) { + .workspace { + grid-template-columns: 300px 1fr 400px; /* Sidebar, list, detail */ + } +} +``` + +### 2. **Touch-Friendly Interactions** +```css +/* Larger touch targets on mobile */ +@media (hover: none) { + button { + min-height: 48px; + min-width: 48px; + } + + .message-row { + padding: 12px 16px; + } +} +``` + +--- + +## Dark Mode Support + +```css +/* Automatic dark mode */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: #1a1a1a; + --bg-secondary: #2a2a2a; + --text-primary: #e0e0e0; + --text-secondary: #b0b0b0; + } +} + +/* Manual dark mode toggle */ +html[data-theme='dark'] { + color-scheme: dark; + --bg-primary: #1a1a1a; +} +``` + +--- + +## Performance Visual Feedback + +### 1. **Loading States** +- Skeleton loaders for first load +- Pulse animation for ongoing operations +- Progress bars for long operations + +### 2. **Action Feedback** +- Immediate button state change +- Checkmark icon appears briefly +- Toast notification confirms action + +### 3. **Error States** +- Clear error messages +- Helpful suggestions +- Retry buttons + +--- + +## Comparison with Gmail/Outlook + +| Feature | Gmail ✓ | Outlook ✓ | Flow (After) | +|---------|---------|----------|-------------| +| Smooth animations | ✓ | ✓ | ✓ | +| Dark mode | ✓ | ✓ | ✓ | +| Keyboard shortcuts | ✓ | ✓ | ✓ | +| Toast notifications | ✓ | ✓ | ✓ | +| Skeleton loaders | ✓ | ✓ | ✓ | +| Search suggestions | ✓ | ✓ | ✓ | +| Optimistic updates | ✓ | ✓ | ✓ | +| Responsive design | ✓ | ✓ | ✓ | +| Accessibility | ✓ | ✓ | ✓ | + +--- + +## Implementation Priority + +**Phase 1 (High Impact):** +- [ ] Debounced search +- [ ] Memoized components +- [ ] Toast notifications +- [ ] Better colors/spacing + +**Phase 2 (Medium Impact):** +- [ ] Keyboard shortcuts +- [ ] Skeleton loaders +- [ ] Optimistic updates +- [ ] Dark mode polish + +**Phase 3 (Polish):** +- [ ] Search suggestions +- [ ] Advanced animations +- [ ] Accessibility audit +- [ ] Mobile optimization + From d809e309959ffe82876cc92c2f01a94e71cc48cc Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:25:52 +0200 Subject: [PATCH 2/8] feat: Add debounce hook for search optimization --- src/hooks/useDebounce.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/hooks/useDebounce.ts diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 0000000..bc8d09d --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +export function useDebounce(value: T, delay: number): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => clearTimeout(handler); + }, [value, delay]); + + return debouncedValue; +} From 4cbb0d6cc39a5fbe0f70fa8f283ab6d49c1c5066 Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:26:17 +0200 Subject: [PATCH 3/8] feat: Add async hook for better async state management --- src/hooks/useAsync.ts | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/hooks/useAsync.ts diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts new file mode 100644 index 0000000..30ee261 --- /dev/null +++ b/src/hooks/useAsync.ts @@ -0,0 +1,50 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +interface AsyncState { + status: 'idle' | 'pending' | 'success' | 'error'; + data: T | null; + error: Error | null; +} + +export function useAsync( + asyncFunction: () => Promise, + immediate = true, +): AsyncState { + const [state, setState] = useState>({ + status: 'idle', + data: null, + error: null, + }); + + const mountedRef = useRef(true); + + const execute = useCallback(async () => { + setState({ status: 'pending', data: null, error: null }); + try { + const response = await asyncFunction(); + if (mountedRef.current) { + setState({ status: 'success', data: response, error: null }); + } + } catch (error) { + if (mountedRef.current) { + setState({ + status: 'error', + data: null, + error: error instanceof Error ? error : new Error(String(error)), + }); + } + } + }, [asyncFunction]); + + useEffect(() => { + if (immediate) { + execute(); + } + + return () => { + mountedRef.current = false; + }; + }, [execute, immediate]); + + return state; +} From 368c4ea4bf079f189642311b9fd930f9cc878b9f Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:27:40 +0200 Subject: [PATCH 4/8] feat: Add performance monitoring utilities --- src/lib/performance.ts | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/lib/performance.ts diff --git a/src/lib/performance.ts b/src/lib/performance.ts new file mode 100644 index 0000000..b98fc03 --- /dev/null +++ b/src/lib/performance.ts @@ -0,0 +1,48 @@ +/** + * Performance monitoring and optimization utilities + */ + +export interface PerformanceMetrics { + renderTime: number; + searchTime: number; + apiTime: number; +} + +const metrics: PerformanceMetrics = { + renderTime: 0, + searchTime: 0, + apiTime: 0, +}; + +export function measurePerformance(name: string, fn: () => void): number { + const start = performance.now(); + fn(); + const end = performance.now(); + const duration = end - start; + + if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') { + console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`); + } + + return duration; +} + +export async function measureAsyncPerformance( + name: string, + fn: () => Promise, +): Promise { + const start = performance.now(); + const result = await fn(); + const end = performance.now(); + const duration = end - start; + + if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') { + console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`); + } + + return result; +} + +export function reportMetrics(): PerformanceMetrics { + return metrics; +} From 35ec23effa8ba2ffb630d4a08bb0218afef2c005 Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:27:57 +0200 Subject: [PATCH 5/8] feat: Extract and memoize MessageRow component for better performance --- src/components/MessageRow.tsx | 100 ++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/components/MessageRow.tsx diff --git a/src/components/MessageRow.tsx b/src/components/MessageRow.tsx new file mode 100644 index 0000000..a3550ff --- /dev/null +++ b/src/components/MessageRow.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { Star } from 'lucide-react'; +import type { MouseEvent } from 'react'; +import { memo } from 'react'; +import { ContactHoverCard } from '@/components/flow-console/ContactHoverCard'; +import { formatListDate, getMessageFolderLabel, sentTrackingKind, sentTrackingLabel } from '@/lib/flow-console/format'; +import { contactFromMessage } from '@/lib/flow-console/mail'; +import type { MailThread } from '@/lib/flow-console/types'; +import styles from './FlowConsole.module.css'; + +interface MessageRowProps { + thread: MailThread; + isSelected: boolean; + onSelect: (threadId: string) => void; + onToggleSelect: (threadId: string) => void; + onToggleStarred: (event: MouseEvent, messageId: string) => void; + onOpenCompose: (contact: any) => void; + onShowStatus: (tool: string, contact: any) => void; + onKeyDown: (event: React.KeyboardEvent, threadId: string) => void; +} + +export const MessageRow = memo(function MessageRow({ + thread, + isSelected, + onSelect, + onToggleSelect, + onToggleStarred, + onOpenCompose, + onShowStatus, + onKeyDown, +}: MessageRowProps) { + const message = thread.latest; + const contact = contactFromMessage(message); + + return ( +
onSelect(thread.id)} + onKeyDown={(event) => onKeyDown(event, thread.id)} + role="button" + tabIndex={0} + > + onToggleSelect(thread.id)} + onClick={(event) => event.stopPropagation()} + type="checkbox" + /> + + event.stopPropagation()}> + + + + + + + {thread.subject} + {message.preview || message.body ? ( + - {message.preview || message.body} + ) : null} + {message.direction === 'outbound' ? ( + + {sentTrackingLabel(message, true)} + + ) : null} + {thread.count > 1 ? ( + {thread.count} + ) : null} + + +
+ ); +}); From 133f491ab01979f15b8ba296d819ac491a356a85 Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:38:27 +0200 Subject: [PATCH 6/8] Update src/components/MessageRow.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/components/MessageRow.tsx | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/MessageRow.tsx b/src/components/MessageRow.tsx index a3550ff..cdd7323 100644 --- a/src/components/MessageRow.tsx +++ b/src/components/MessageRow.tsx @@ -15,10 +15,27 @@ interface MessageRowProps { onSelect: (threadId: string) => void; onToggleSelect: (threadId: string) => void; onToggleStarred: (event: MouseEvent, messageId: string) => void; - onOpenCompose: (contact: any) => void; - onShowStatus: (tool: string, contact: any) => void; +import { Star } from 'lucide-react'; +import type { MouseEvent } from 'react'; +import { memo } from 'react'; +import { ContactHoverCard } from '`@/components/flow-console/ContactHoverCard`'; +import type { ContactPreview } from '`@/lib/flow-console/types`'; +import { formatListDate, getMessageFolderLabel, sentTrackingKind, sentTrackingLabel } from '`@/lib/flow-console/format`'; +import { contactFromMessage } from '`@/lib/flow-console/mail`'; +import type { MailThread } from '`@/lib/flow-console/types`'; +import styles from './FlowConsole.module.css'; + +interface MessageRowProps { + thread: MailThread; + isSelected: boolean; + onSelect: (threadId: string) => void; + onToggleSelect: (threadId: string) => void; + onToggleStarred: (event: MouseEvent, messageId: string) => void; + onOpenCompose: (contact: ContactPreview) => void; + onShowStatus: (tool: string, contact: ContactPreview) => void; onKeyDown: (event: React.KeyboardEvent, threadId: string) => void; } +} export const MessageRow = memo(function MessageRow({ thread, From 4b6791a332aaa843562cd982f8a8276a44255559 Mon Sep 17 00:00:00 2001 From: sitholevunene373-lab Date: Fri, 19 Jun 2026 19:39:51 +0200 Subject: [PATCH 7/8] Update UX_IMPROVEMENTS.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- UX_IMPROVEMENTS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/UX_IMPROVEMENTS.md b/UX_IMPROVEMENTS.md index d6bc5f4..2937b17 100644 --- a/UX_IMPROVEMENTS.md +++ b/UX_IMPROVEMENTS.md @@ -187,18 +187,23 @@ useEffect(() => { switch (e.key) { case 'j': openThreadByOffset(1); // Next + e.preventDefault(); break; case 'k': openThreadByOffset(-1); // Previous + e.preventDefault(); break; case 'c': setComposeOpen(true); + e.preventDefault(); break; case 'r': selectedThread && replyToMessage(selectedThread.messages[0]); + e.preventDefault(); break; case 'e': selectedThread && archiveOpenThread(); + e.preventDefault(); break; } }; From b3ae90dabcf1a48c204423286ac1ab8b80c4bcf1 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:50:52 +0000 Subject: [PATCH 8/8] fix: apply CodeRabbit auto-fixes Fixed 3 file(s) based on 7 unresolved review comments. Co-authored-by: CodeRabbit --- UX_IMPROVEMENTS.md | 117 ++++++++++++++++++++++++++++++++--------- src/hooks/useAsync.ts | 2 + src/lib/performance.ts | 20 ++++++- 3 files changed, 114 insertions(+), 25 deletions(-) diff --git a/UX_IMPROVEMENTS.md b/UX_IMPROVEMENTS.md index 2937b17..3f28fce 100644 --- a/UX_IMPROVEMENTS.md +++ b/UX_IMPROVEMENTS.md @@ -34,19 +34,22 @@ --- ### 2. **Premium Color Palette** 🎨 + +**Note:** These tokens are defined as aliases to maintain compatibility with existing `src/app/styles/base.css` variables (--flow-muted, --blue, --red, etc.). Components should use these semantic names for consistency. + ```css :root { - /* Primary Colors */ - --color-primary: #1a73e8; - --color-primary-hover: #1765cc; + /* Primary Colors - maps to existing --blue */ + --color-primary: var(--blue); + --color-primary-hover: var(--blue-dark); --color-primary-focus: #1e40af; - - /* Semantic Colors */ - --color-success: #0d652d; - --color-warning: #b3930f; - --color-error: #d93025; - --color-info: #1a73e8; - + + /* Semantic Colors - maps to existing semantic colors */ + --color-success: var(--green); + --color-warning: var(--yellow); + --color-error: var(--red); + --color-info: var(--blue); + /* Neutral Grays (like Gmail) */ --color-gray-50: #f8f9fa; --color-gray-100: #f3f3f3; @@ -55,18 +58,18 @@ --color-gray-400: #bdc1c6; --color-gray-500: #9aa0a6; --color-gray-600: #80868b; - --color-gray-700: #5f6368; + --color-gray-700: var(--flow-muted); /* Maps to existing --flow-muted */ --color-gray-800: #3c4043; --color-gray-900: #202124; - + /* Background Colors */ - --color-bg: #ffffff; - --color-bg-hover: #f8f9fa; + --color-bg: var(--panel); + --color-bg-hover: var(--panel-soft); --color-bg-selected: #fce8e6; - + /* Text Colors */ --color-text-primary: #202124; - --color-text-secondary: #5f6368; + --color-text-secondary: var(--flow-muted); --color-text-disabled: #9aa0a6; } @@ -81,6 +84,11 @@ } ``` +**Migration Strategy:** +- New tokens are defined as aliases to existing base.css variables where possible +- Components should gradually migrate to use semantic --color-* tokens +- Existing --blue, --red, --flow-muted tokens remain as base values + --- ### 3. **Spacing System** 📐 @@ -103,20 +111,29 @@ --- ### 4. **Elevation & Shadows** ✨ + +**Note:** Border-radius tokens defined here complement the existing --radius (0.625rem) in base.css. The existing --radius maps to --radius-md for backwards compatibility. + ```css :root { --shadow-sm: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15); --shadow-md: 0 1px 3px 0 rgba(60, 64, 67, 0.3), 0 4px 8px 3px rgba(60, 64, 67, 0.15); --shadow-lg: 0 4px 6px 3px rgba(60, 64, 67, 0.15), 0 12px 16px 4px rgba(60, 64, 67, 0.1); --shadow-xl: 0 8px 12px 6px rgba(60, 64, 67, 0.15), 0 20px 25px 6px rgba(60, 64, 67, 0.1); - + + /* Border radius scale - --radius from base.css (0.625rem/10px) maps to --radius-md */ --radius-sm: 2px; - --radius-md: 4px; + --radius-md: 4px; /* Existing --radius (0.625rem) can be aliased here if needed */ --radius-lg: 8px; --radius-full: 9999px; } ``` +**Migration Strategy:** +- Existing components using --radius should continue to work +- New components should use the appropriate scale value (--radius-sm, --radius-md, --radius-lg) +- Consider aliasing: `--radius-md: var(--radius);` to maintain consistency + --- ### 5. **Smooth Animations** 🎬 @@ -302,7 +319,7 @@ function showToast(message: string, type: Toast['type'] = 'info', duration = 300 {toasts.map(toast => (
('auto'); + + useEffect(() => { + const storedTheme = localStorage.getItem('theme') as typeof theme; + if (storedTheme) setTheme(storedTheme); + }, []); + + useEffect(() => { + if (theme === 'auto') { + document.documentElement.removeAttribute('data-theme'); + } else { + document.documentElement.setAttribute('data-theme', theme); + } + localStorage.setItem('theme', theme); + }, [theme]); + + return { theme, setTheme }; +} +``` + +**How it works:** +1. No `data-theme` attribute: System preference applies via `@media (prefers-color-scheme: dark)` +2. `data-theme="dark"`: Dark mode, regardless of system preference +3. `data-theme="light"`: Light mode, regardless of system preference + --- ## Performance Visual Feedback diff --git a/src/hooks/useAsync.ts b/src/hooks/useAsync.ts index 30ee261..20e5d53 100644 --- a/src/hooks/useAsync.ts +++ b/src/hooks/useAsync.ts @@ -37,6 +37,8 @@ export function useAsync( }, [asyncFunction]); useEffect(() => { + mountedRef.current = true; + if (immediate) { execute(); } diff --git a/src/lib/performance.ts b/src/lib/performance.ts index b98fc03..4f70b7e 100644 --- a/src/lib/performance.ts +++ b/src/lib/performance.ts @@ -24,6 +24,15 @@ export function measurePerformance(name: string, fn: () => void): number { console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`); } + // Update metrics based on the measurement name + if (name.toLowerCase().includes('render')) { + metrics.renderTime = duration; + } else if (name.toLowerCase().includes('search')) { + metrics.searchTime = duration; + } else if (name.toLowerCase().includes('api')) { + metrics.apiTime = duration; + } + return duration; } @@ -40,9 +49,18 @@ export async function measureAsyncPerformance( console.log(`[Performance] ${name}: ${duration.toFixed(2)}ms`); } + // Update metrics based on the measurement name + if (name.toLowerCase().includes('render')) { + metrics.renderTime = duration; + } else if (name.toLowerCase().includes('search')) { + metrics.searchTime = duration; + } else if (name.toLowerCase().includes('api')) { + metrics.apiTime = duration; + } + return result; } export function reportMetrics(): PerformanceMetrics { - return metrics; + return { ...metrics }; }