mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
fix(ui): improve split quota bar readability
This commit is contained in:
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'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user