- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid - Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px) - Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation - Modified files across all components to ensure consistent 8px grid alignment - Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
131 lines
3.7 KiB
TypeScript
131 lines
3.7 KiB
TypeScript
/**
|
|
* Rate Limit Indicator Component
|
|
* Action 5.4.1.2: Display rate limit status to users
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { useRateLimitStore } from '@/stores/rateLimit';
|
|
import { AlertTriangle, Clock } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* RateLimitIndicator - Displays current rate limit status
|
|
*
|
|
* Shows rate limit information when:
|
|
* - User is rate limited (isLimited = true)
|
|
* - Remaining requests are low (< 20% of limit)
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* <RateLimitIndicator />
|
|
* ```
|
|
*/
|
|
export function RateLimitIndicator() {
|
|
const { limit, remaining, reset, isLimited } = useRateLimitStore();
|
|
const [timeUntilReset, setTimeUntilReset] = useState<number | null>(null);
|
|
|
|
// Calculate time until reset
|
|
useEffect(() => {
|
|
if (!reset) {
|
|
setTimeUntilReset(null);
|
|
return;
|
|
}
|
|
|
|
const updateTimer = () => {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const secondsUntilReset = reset - now;
|
|
setTimeUntilReset(secondsUntilReset > 0 ? secondsUntilReset : 0);
|
|
};
|
|
|
|
// Update immediately
|
|
updateTimer();
|
|
|
|
// Update every second
|
|
const interval = setInterval(updateTimer, 1000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [reset]);
|
|
|
|
// Calculate percentage remaining
|
|
const percentageRemaining =
|
|
limit !== null && remaining !== null && limit > 0
|
|
? (remaining / limit) * 100
|
|
: null;
|
|
|
|
// Show indicator if:
|
|
// 1. User is rate limited, OR
|
|
// 2. Remaining is low (< 20% of limit) and we have data
|
|
const shouldShow =
|
|
isLimited ||
|
|
(limit !== null &&
|
|
remaining !== null &&
|
|
percentageRemaining !== null &&
|
|
percentageRemaining < 20);
|
|
|
|
if (!shouldShow || limit === null) {
|
|
return null;
|
|
}
|
|
|
|
// Format time until reset
|
|
const formatTimeUntilReset = (seconds: number): string => {
|
|
if (seconds <= 0) return '0s';
|
|
if (seconds < 60) return `${seconds}s`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = seconds % 60;
|
|
if (minutes < 60) {
|
|
return remainingSeconds > 0
|
|
? `${minutes}m ${remainingSeconds}s`
|
|
: `${minutes}m`;
|
|
}
|
|
const hours = Math.floor(minutes / 60);
|
|
const remainingMinutes = minutes % 60;
|
|
return remainingMinutes > 0
|
|
? `${hours}h ${remainingMinutes}m`
|
|
: `${hours}h`;
|
|
};
|
|
|
|
// Determine severity and styling
|
|
const isCritical = isLimited || (remaining !== null && remaining <= 0);
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-2 px-4 py-1.5 rounded-lg text-xs font-medium transition-all',
|
|
isCritical
|
|
? 'bg-kodo-red/10 text-kodo-red border border-kodo-red/30'
|
|
: 'bg-kodo-gold/10 text-kodo-gold border border-kodo-gold/30',
|
|
)}
|
|
role="alert"
|
|
aria-live="polite"
|
|
>
|
|
<AlertTriangle className="w-4 h-4 flex-shrink-0" />
|
|
<div className="flex items-center gap-2">
|
|
{isLimited ? (
|
|
<>
|
|
<span>Rate limit exceeded</span>
|
|
{timeUntilReset !== null && (
|
|
<span className="flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{formatTimeUntilReset(timeUntilReset)}
|
|
</span>
|
|
)}
|
|
</>
|
|
) : (
|
|
<>
|
|
<span>
|
|
{remaining !== null
|
|
? `${remaining}/${limit} requests`
|
|
: `${limit} requests`}
|
|
</span>
|
|
{timeUntilReset !== null && (
|
|
<span className="flex items-center gap-1 opacity-75">
|
|
<Clock className="w-3 h-3" />
|
|
resets in {formatTimeUntilReset(timeUntilReset)}
|
|
</span>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|