Skip to content

Commit 0efb2c7

Browse files
committed
feat(accounts): refine card quota interactions
1 parent df9df69 commit 0efb2c7

5 files changed

Lines changed: 56 additions & 35 deletions

File tree

frontend/src/features/accounts/components/AccountDetailPrimitives.tsx

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,21 @@ const toneClassNames: Record<AccountDetailTone, string> = {
1515
};
1616

1717
const sectionDensityClassNames: Record<AccountDetailSectionDensity, string> = {
18-
standard: 'gap-4 py-5',
19-
dense: 'gap-3 py-4',
20-
hero: 'gap-5 py-6',
18+
standard: 'gap-3 py-4',
19+
dense: 'gap-2.5 py-3',
20+
hero: 'gap-4 py-5',
2121
};
2222

2323
const cardSectionDensityClassNames: Record<AccountDetailSectionDensity, string> = {
24-
standard: 'gap-3 p-4',
25-
dense: 'gap-3 p-3',
26-
hero: 'gap-4 p-5',
24+
standard: 'gap-2.5 p-3',
25+
dense: 'gap-2 p-2.5',
26+
hero: 'gap-3 p-4',
27+
};
28+
29+
const sectionBodyDensityClassNames: Record<AccountDetailSectionDensity, string> = {
30+
standard: 'space-y-3',
31+
dense: 'space-y-2.5',
32+
hero: 'space-y-4',
2733
};
2834

2935
interface AccountDetailSectionProps {
@@ -58,26 +64,26 @@ export function AccountDetailSectionHeader({
5864
return (
5965
<div
6066
data-account-detail-section-header="standard"
61-
className={`flex min-w-0 flex-col gap-3 border-b border-dashed border-[var(--border-color)] pb-3 sm:flex-row sm:items-start sm:justify-between ${className}`}
67+
className={`flex min-w-0 flex-col gap-2 border-b border-dashed border-[var(--border-color)] pb-2 sm:flex-row sm:items-start sm:justify-between ${className}`}
6268
>
63-
<div className="min-w-0 space-y-1.5">
69+
<div className="min-w-0 space-y-1">
6470
{eyebrow ? (
65-
<div className="font-mono text-[length:var(--font-size-ui-2xs)] font-black uppercase tracking-[0.22em] text-[var(--text-muted)]">
71+
<div className="font-mono text-[length:var(--font-size-ui-2xs)] font-black uppercase tracking-[0.18em] text-[var(--text-muted)]">
6672
{eyebrow}
6773
</div>
6874
) : null}
6975
{title ? (
70-
<h3 className="text-[length:var(--font-size-ui-sm)] font-black uppercase italic leading-tight tracking-[0.08em] text-[var(--text-primary)]">
76+
<h3 className="text-[length:var(--font-size-ui-xs)] font-black uppercase italic leading-snug tracking-[0.06em] text-[var(--text-primary)]">
7177
{title}
7278
</h3>
7379
) : null}
7480
{meta ? (
75-
<div className="break-words font-mono text-[length:var(--font-size-ui-2xs)] font-black uppercase tracking-[0.12em] text-[var(--text-muted)]">
81+
<div className="break-words font-mono text-[length:var(--font-size-ui-2xs)] font-black uppercase tracking-[0.08em] text-[var(--text-muted)]">
7682
{meta}
7783
</div>
7884
) : null}
7985
</div>
80-
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2">{actions}</div> : null}
86+
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-1.5">{actions}</div> : null}
8187
</div>
8288
);
8389
}
@@ -122,7 +128,7 @@ export function AccountDetailSection({
122128
actions={actions}
123129
/>
124130
) : null}
125-
<div className="min-w-0 space-y-4">
131+
<div data-account-detail-section-body="compact" className={`min-w-0 ${sectionBodyDensityClassNames[density]}`}>
126132
{children}
127133
</div>
128134
</section>

frontend/src/features/accounts/components/CardSections.tsx

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState } from 'react';
2-
import type { MouseEvent } from 'react';
2+
import type { KeyboardEvent, MouseEvent } from 'react';
33
import type { ApiFormat, BillingDisplay } from '../../../types';
44
import type { AccountRecord, QuotaDisplay, QuotaWindowDisplay, Translator } from '../model/types';
55
import type { AccountUsageSummary } from '../model/accountUsage';
@@ -220,18 +220,37 @@ export function QuotaBars({ quotaDisplay, t }: QuotaBarsProps) {
220220
const refreshing = quotaDisplay.refreshing === true;
221221
const hasTokenProgress = windows.some(hasQuotaTokenProgress);
222222

223-
const handleToggleDisplayMode = (event: MouseEvent<HTMLButtonElement>) => {
224-
event.stopPropagation();
223+
const toggleDisplayMode = () => {
225224
if (!hasTokenProgress) return;
226225
setDisplayMode((current) => current === 'percent' ? 'tokens' : 'percent');
227226
};
228227

228+
const handleToggleDisplayMode = (event: MouseEvent<HTMLElement>) => {
229+
event.stopPropagation();
230+
toggleDisplayMode();
231+
};
232+
233+
const handleToggleDisplayModeKeyDown = (event: KeyboardEvent<HTMLElement>) => {
234+
if (event.key !== 'Enter' && event.key !== ' ') return;
235+
event.preventDefault();
236+
event.stopPropagation();
237+
toggleDisplayMode();
238+
};
239+
229240
return (
230-
<div
231-
className="grid gap-2.5 border-b border-dashed border-[var(--border-color)] px-4 py-3"
241+
<section
242+
className={`grid gap-2.5 border-b border-dashed border-[var(--border-color)] px-4 py-3 ${
243+
hasTokenProgress ? 'cursor-pointer transition-colors hover:bg-[var(--bg-surface)]' : ''
244+
}`}
232245
aria-busy={refreshing}
246+
aria-pressed={hasTokenProgress ? displayMode === 'tokens' : undefined}
247+
role={hasTokenProgress ? 'button' : undefined}
248+
tabIndex={hasTokenProgress ? 0 : undefined}
233249
data-quota-refreshing={refreshing ? 'true' : undefined}
234250
data-quota-display-mode={displayMode}
251+
data-account-card-ignore-click={hasTokenProgress ? 'true' : undefined}
252+
onClick={hasTokenProgress ? handleToggleDisplayMode : undefined}
253+
onKeyDown={hasTokenProgress ? handleToggleDisplayModeKeyDown : undefined}
235254
>
236255
{windows.map((window) => {
237256
const resetTime = formatQuotaResetDisplayWithUnix(window.resetLabel, window.resetAtUnix);
@@ -253,21 +272,9 @@ export function QuotaBars({ quotaDisplay, t }: QuotaBarsProps) {
253272
<div className="min-w-0 truncate font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.12em] text-[var(--text-muted)]">
254273
{window.label}
255274
</div>
256-
{hasTokenProgress ? (
257-
<button
258-
type="button"
259-
className="shrink-0 cursor-pointer text-right font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.08em] text-[var(--text-primary)] hover:underline"
260-
aria-pressed={displayMode === 'tokens'}
261-
title={`${formatQuotaPercent(window)} / ${formatQuotaTokenProgress(window)}`}
262-
onClick={handleToggleDisplayMode}
263-
>
264-
{valueLabel}
265-
</button>
266-
) : (
267-
<div className="shrink-0 text-right font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.08em] text-[var(--text-primary)]">
268-
{valueLabel}
269-
</div>
270-
)}
275+
<div className="shrink-0 text-right font-mono text-[length:var(--font-size-ui-sm)] font-black uppercase tracking-[0.08em] text-[var(--text-primary)]">
276+
{valueLabel}
277+
</div>
271278
</div>
272279
<div className="grid min-w-0 gap-1">
273280
<div
@@ -299,7 +306,7 @@ export function QuotaBars({ quotaDisplay, t }: QuotaBarsProps) {
299306
</div>
300307
);
301308
})}
302-
</div>
309+
</section>
303310
);
304311
}
305312

frontend/src/features/accounts/tests/accountCardLayout.test.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ test('quota rows keep label and percentage together above the progress bar', asy
8181
const styleSource = await readFile(new URL('../../../style.css', import.meta.url), 'utf8');
8282

8383
assert.match(source, /account-card-quota-heading/);
84-
assert.match(source, /className="grid gap-2\.5 border-b/);
84+
assert.match(source, /className=\{`grid gap-2\.5 border-b border-dashed/);
8585
assert.match(source, /className="account-card-quota-row grid min-w-0 gap-1\.5"/);
8686
assert.doesNotMatch(styleSource, /\.account-card-quota-row\s*\{[^}]*grid-template-columns:\s*4\.25rem/s);
8787
});
@@ -91,7 +91,12 @@ test('quota bars can toggle from percent to token progress when token counts exi
9191

9292
assert.match(source, /useState<QuotaBarsDisplayMode>\('percent'\)/);
9393
assert.match(source, /data-quota-display-mode=\{displayMode\}/);
94+
assert.match(source, /onClick=\{hasTokenProgress \? handleToggleDisplayMode : undefined\}/);
95+
assert.match(source, /onKeyDown=\{hasTokenProgress \? handleToggleDisplayModeKeyDown : undefined\}/);
96+
assert.match(source, /role=\{hasTokenProgress \? 'button' : undefined\}/);
97+
assert.match(source, /data-account-card-ignore-click=\{hasTokenProgress \? 'true' : undefined\}/);
9498
assert.match(source, /setDisplayMode\(\(current\) => current === 'percent' \? 'tokens' : 'percent'\)/);
9599
assert.match(source, /formatQuotaTokenProgress\(window\)/);
96100
assert.match(source, /event\.stopPropagation\(\)/);
101+
assert.doesNotMatch(source, /<button[\s\S]*handleToggleDisplayMode[\s\S]*<\/button>/);
97102
});

frontend/src/features/design-system/storyCatalog.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,8 @@ test('account detail modules expose design-system anatomy and runtime states', a
327327
assert.match(primitivesSource, /AccountDetailModuleStackLayout/);
328328
assert.match(primitivesSource, /AccountDetailSectionHeader/);
329329
assert.match(primitivesSource, /data-account-detail-section-header="standard"/);
330+
assert.match(primitivesSource, /data-account-detail-section-body="compact"/);
331+
assert.doesNotMatch(primitivesSource, /<div className="min-w-0 space-y-4">/);
330332
assert.match(primitivesSource, /AccountDetailOverviewGrid/);
331333
assert.match(primitivesSource, /data-account-detail-overview-grid="runtime-evidence"/);
332334
assert.match(primitivesSource, /data-account-detail-overview-equal-height="true"/);

frontend/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface BillingDisplay {
4646
balances: { currency: string; totalBalance: string; grantedBalance: string; toppedUpBalance: string }[];
4747
}
4848
export type CredentialSource = 'auth-file' | 'api-key';
49+
export type AccountPlanType = 'free' | 'plus' | 'pro';
4950

5051
export type ApiFormat = 'anthropic' | 'openai_chat' | 'openai_responses' | 'gemini_native';
5152

0 commit comments

Comments
 (0)