fix(ui): improve split quota bar readability

This commit is contained in:
Tam Nhu Tran committed 2026-04-30 10:32:43 -04:00
1 parent a8b686d643
commit 476c6d827f
2 files changed
+70 -44

No files matched your search

@@ -49,6 +49,17 @@ function getQuotaColor(percentage: number): string {
return 'bg-green-500'; return 'bg-green-500';
} }
function getQuotaTextColor(percentage: number): string {
const clamped = Math.max(0, Math.min(100, percentage));
if (clamped <= 20) return 'text-red-600 dark:text-red-400';
if (clamped <= 50) return 'text-amber-600 dark:text-amber-400';
return 'text-emerald-600 dark:text-emerald-400';
}
function getDisplayQuotaValue(value: number): number {
return Number(formatQuotaPercent(value));
}
function formatRelativeTime(dateStr: string | undefined): string { function formatRelativeTime(dateStr: string | undefined): string {
if (!dateStr) return ''; if (!dateStr) return '';
@@ -174,59 +185,66 @@ export function AccountQuotaPanel({
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
{mode === 'compact' ? ( {mode === 'compact' ? (
<div className="space-y-0.5 cursor-help"> <div className="cursor-help">
<div className="flex items-center justify-between">
<span className="text-[8px] text-muted-foreground/70 uppercase font-bold tracking-tight">
{t('accountCard.quota')}
</span>
<span
className={cn(
'text-[10px] font-mono font-bold',
minQuotaValue > 50
? 'text-emerald-600 dark:text-emerald-400'
: minQuotaValue > 20
? 'text-amber-500'
: 'text-red-500'
)}
>
{minQuotaLabel}%
</span>
</div>
{quotaRows.length > 0 ? ( {quotaRows.length > 0 ? (
<div className="space-y-0.5"> <div className="space-y-1">
{quotaRows.map((row) => ( {quotaRows.map((row) => (
<div <div
key={row.id} key={row.id}
className="grid grid-cols-[2rem_minmax(0,1fr)_2.25rem] items-center gap-1 text-[7px]" className="grid grid-cols-[2rem_minmax(0,1fr)_2rem] items-center gap-x-1 text-[7px]"
> >
<span className="font-semibold uppercase text-muted-foreground/75"> <span className="font-semibold uppercase leading-none text-muted-foreground/75">
{row.compactLabel} {row.compactLabel}
</span> </span>
<Progress <Progress
value={Math.max(0, Math.min(100, row.value))} value={getDisplayQuotaValue(row.value)}
aria-label={`${row.compactLabel} quota`} aria-label={`${row.compactLabel} quota`}
className="h-1" className="h-1.5"
indicatorClassName={getQuotaColor(row.value)} indicatorClassName={getQuotaColor(row.value)}
/> />
<span className="text-right font-mono font-semibold text-foreground/80"> <span
className={cn(
'text-right font-mono text-[10px] font-bold leading-none',
getQuotaTextColor(row.value)
)}
>
{formatQuotaPercent(row.value)}% {formatQuotaPercent(row.value)}%
</span> </span>
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<Progress <div className="space-y-0.5">
value={minQuotaValue} <div className="flex items-center justify-between">
aria-label="Quota" <span className="text-[8px] text-muted-foreground/70 uppercase font-bold tracking-tight">
className="h-1" {t('accountCard.quota')}
indicatorClassName={ </span>
minQuotaValue > 50 <span
? 'bg-emerald-500' className={cn(
: minQuotaValue > 20 'text-[10px] font-mono font-bold',
? 'bg-amber-500' minQuotaValue > 50
: 'bg-red-500' ? 'text-emerald-600 dark:text-emerald-400'
} : minQuotaValue > 20
/> ? 'text-amber-500'
: 'text-red-500'
)}
>
{minQuotaLabel}%
</span>
</div>
<Progress
value={minQuotaValue}
aria-label="Quota"
className="h-1"
indicatorClassName={
minQuotaValue > 50
? 'bg-emerald-500'
: minQuotaValue > 20
? 'bg-amber-500'
: 'bg-red-500'
}
/>
</div>
)} )}
</div> </div>
) : ( ) : (
@@ -265,12 +283,17 @@ export function AccountQuotaPanel({
<span className="min-w-0 truncate text-muted-foreground"> <span className="min-w-0 truncate text-muted-foreground">
{row.label} {row.label}
</span> </span>
<span className="shrink-0 font-mono font-semibold text-foreground/80"> <span
className={cn(
'shrink-0 font-mono font-semibold',
getQuotaTextColor(row.value)
)}
>
{formatQuotaPercent(row.value)}% {formatQuotaPercent(row.value)}%
</span> </span>
</div> </div>
<Progress <Progress
value={Math.max(0, Math.min(100, row.value))} value={getDisplayQuotaValue(row.value)}
aria-label={`${row.label} quota`} aria-label={`${row.label} quota`}
className="h-2" className="h-2"
indicatorClassName={getQuotaColor(row.value)} indicatorClassName={getQuotaColor(row.value)}
@@ -3,6 +3,9 @@ import { describe, expect, it } from 'vitest';
import { AccountQuotaPanel } from '@/components/account/shared/account-quota-panel'; import { AccountQuotaPanel } from '@/components/account/shared/account-quota-panel';
import type { ClaudeQuotaResult, CodexQuotaResult, GeminiCliQuotaResult } from '@/lib/api-client'; import type { ClaudeQuotaResult, CodexQuotaResult, GeminiCliQuotaResult } from '@/lib/api-client';
const NOISY_WEEKLY_REMAINING_PERCENT = 45 - 1e-14;
const NOISY_WEEKLY_USED_PERCENT = 100 - NOISY_WEEKLY_REMAINING_PERCENT;
function createGeminiFailureQuota(): GeminiCliQuotaResult { function createGeminiFailureQuota(): GeminiCliQuotaResult {
return { return {
success: false, success: false,
@@ -37,9 +40,9 @@ function createClaudeQuota(): ClaudeQuotaResult {
rateLimitType: 'seven_day_sonnet', rateLimitType: 'seven_day_sonnet',
label: 'Weekly usage', label: 'Weekly usage',
status: 'allowed', status: 'allowed',
utilization: 0.6, utilization: NOISY_WEEKLY_USED_PERCENT / 100,
usedPercent: 60, usedPercent: NOISY_WEEKLY_USED_PERCENT,
remainingPercent: 40, remainingPercent: NOISY_WEEKLY_REMAINING_PERCENT,
resetAt: '2026-05-07T01:00:00.000Z', resetAt: '2026-05-07T01:00:00.000Z',
}, },
], ],
@@ -54,7 +57,7 @@ function createClaudeQuota(): ClaudeQuotaResult {
weekly: { weekly: {
rateLimitType: 'seven_day_sonnet', rateLimitType: 'seven_day_sonnet',
label: 'Weekly usage', label: 'Weekly usage',
remainingPercent: 40, remainingPercent: NOISY_WEEKLY_REMAINING_PERCENT,
resetAt: '2026-05-07T01:00:00.000Z', resetAt: '2026-05-07T01:00:00.000Z',
status: 'allowed', status: 'allowed',
}, },
@@ -137,14 +140,14 @@ describe('AccountQuotaPanel quota bars', () => {
expect(screen.getByText('5h')).toBeInTheDocument(); expect(screen.getByText('5h')).toBeInTheDocument();
expect(screen.getByText('Week')).toBeInTheDocument(); expect(screen.getByText('Week')).toBeInTheDocument();
expect(screen.getByText('43%')).toBeInTheDocument(); expect(screen.getByText('43%')).toBeInTheDocument();
expect(screen.getAllByText('40%').length).toBeGreaterThan(0); expect(screen.getAllByText('45%').length).toBeGreaterThan(0);
expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute( expect(screen.getByRole('progressbar', { name: '5h quota' })).toHaveAttribute(
'aria-valuenow', 'aria-valuenow',
'43' '43'
); );
expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute( expect(screen.getByRole('progressbar', { name: 'Week quota' })).toHaveAttribute(
'aria-valuenow', 'aria-valuenow',
'40' '45'
); );
}); });