optimize spinner

This commit is contained in:
Zane Staggs
2025-12-01 13:24:43 -08:00
parent f2ed0d65e1
commit fedc1d8d2b
3 changed files with 116 additions and 36 deletions
@@ -280,18 +280,21 @@ const AppSidebar: React.FC<SidebarProps> = ({ currentPath }) => {
return currentPath === path;
};
const handleNewChat = async () => {
const handleNewChat = React.useCallback(async () => {
await startNewSession('', setView);
};
}, [setView]);
const handleSessionClick = async (session: Session) => {
markSessionActive(session.id);
resumeSession(session, setView);
};
const handleSessionClick = React.useCallback(
async (session: Session) => {
markSessionActive(session.id);
resumeSession(session, setView);
},
[markSessionActive, setView]
);
const handleViewAllClick = () => {
const handleViewAllClick = React.useCallback(() => {
navigate('/sessions');
};
}, [navigate]);
const renderMenuItem = (entry: NavigationEntry, index: number) => {
if (entry.type === 'separator') {
+35 -28
View File
@@ -1,4 +1,6 @@
import { Loader2, AlertCircle } from 'lucide-react';
import { AlertCircle } from 'lucide-react';
import React from 'react';
import { OptimizedSpinner } from './ui/optimized-spinner';
interface SessionIndicatorsProps {
isStreaming?: boolean;
@@ -10,33 +12,38 @@ interface SessionIndicatorsProps {
/**
* Visual indicators for session status
* - Error: red alert icon (highest priority)
* - Streaming: animated spinner
* - Streaming: animated spinner with GPU acceleration
* - Has activity: green dot (shown after streaming completes until viewed)
*/
export function SessionIndicators({
isStreaming,
hasUnread,
hasError,
className = '',
}: SessionIndicatorsProps) {
if (!isStreaming && !hasUnread && !hasError) {
return null;
}
export const SessionIndicators = React.memo<SessionIndicatorsProps>(
({ isStreaming, hasUnread, hasError, className = '' }) => {
if (!isStreaming && !hasUnread && !hasError) {
return null;
}
return (
<div className={`flex items-center gap-1 ${className}`}>
{hasError && (
<AlertCircle
className="w-3.5 h-3.5 text-red-500"
aria-label="Session encountered an error"
/>
)}
{isStreaming && !hasError && (
<Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" aria-label="Streaming" />
)}
{hasUnread && !isStreaming && !hasError && (
<div className="w-2 h-2 bg-green-500 rounded-full" aria-label="Has new activity" />
)}
</div>
);
}
return (
<div className={`flex items-center gap-1 ${className}`}>
{hasError && (
<AlertCircle
className="w-3.5 h-3.5 text-red-500"
aria-label="Session encountered an error"
/>
)}
{isStreaming && !hasError && (
<OptimizedSpinner size="xs" className="text-blue-500" aria-label="Streaming" />
)}
{hasUnread && !isStreaming && !hasError && (
<div
className="w-2 h-2 bg-green-500 rounded-full"
aria-label="Has new activity"
style={{
transform: 'translateZ(0)',
}}
/>
)}
</div>
);
}
);
SessionIndicators.displayName = 'SessionIndicators';
@@ -0,0 +1,70 @@
import React from 'react';
import { Loader2 } from 'lucide-react';
import { cn } from '../../utils';
interface OptimizedSpinnerProps {
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
color?: string;
'aria-label'?: string;
}
// Size mappings
const sizeClasses = {
xs: 'w-3 h-3',
sm: 'w-4 h-4',
md: 'w-6 h-6',
lg: 'w-8 h-8',
xl: 'w-10 h-10',
};
/**
* GPU-accelerated spinner component for smooth performance
* Uses CSS transforms and containment for optimal rendering
*/
export const OptimizedSpinner = React.memo<OptimizedSpinnerProps>(
({ className = '', size = 'sm', color, 'aria-label': ariaLabel = 'Loading' }) => {
// Inject optimized keyframes on first mount
React.useEffect(() => {
const styleId = 'optimized-spin-keyframes';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
@keyframes optimized-spin {
from {
transform: rotate(0deg) translateZ(0);
}
to {
transform: rotate(360deg) translateZ(0);
}
}
.spinner-container {
contain: layout style paint;
will-change: transform;
transform: translateZ(0);
backface-visibility: hidden;
perspective: 1000px;
}
`;
document.head.appendChild(style);
}
}, []);
return (
<div
className={cn('spinner-container', sizeClasses[size], className)}
aria-label={ariaLabel}
style={{
animation: 'optimized-spin 1s linear infinite',
color: color,
}}
>
<Loader2 className="w-full h-full" />
</div>
);
}
);
OptimizedSpinner.displayName = 'OptimizedSpinner';