fix(quota): prevent overflow with responsive credential cards

- Remove the redundant page title and table headings
- Adapt card columns to the available content width
- Compact quota details and prevent controls from overlapping
- Add layout regression coverage for 11 viewport configurations

Fixes #342
This commit is contained in:
lzt404 committed 2026-10-05 00:45:17 +08:00
1 parent d4e49a240c
commit a79140b8d2
5 files changed
+124 -81

No files matched your search

+1
View File
@@ -0,0 +1 @@
<html><body><div id="root"></div><script type="module" src="/tests/fixtures/quota-layout.tsx"></script></body></html>
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { I18nProvider } from '../../src/i18n';
import { QuotaCard } from '../../src/pages/QuotaPage';
import { quotaRowsFor } from '../../src/services/quotaService';
import '../../src/styles/index.css';
localStorage.setItem('easy-cli-proxy-api.locale', 'en');
const rows = quotaRowsFor('kimi', {
limits: [{ window: { duration: 5, timeUnit: 'TIME_UNIT_HOUR' }, detail: { limit: 100, remaining: 80 } }],
usage: { limit: 100, remaining: 60 },
});
createRoot(document.getElementById('root')!).render(
<I18nProvider>
<main className="content" style={{ width: 1100, padding: 0 }}>
<section className="page quota-page">
<div className="real-quota-grid">
{[1, 2, 3, 4, 5, 6].map((id) => <QuotaCard key={id} file={{ name: `kimi-${id}-long-credential-name@example.com.json`, provider: 'kimi' }} quota={{ status: 'success', rows }} onRefresh={() => {}} />)}
</div>
<div className="real-quota-grid">
<QuotaCard file={{ name: 'codex.json', provider: 'codex' }} quota={{ status: 'success', rows, resetCredits: 2, resetCreditsApplicable: 2, plan: 'team', subscriptionActiveUntil: '2030-10-17T13:58:00Z', resetCreditsEarliestExpiry: '2030-10-23T05:10:00Z' }} onRefresh={() => {}} onReset={() => {}} />
<QuotaCard file={{ name: 'failed.json', provider: 'kimi' }} quota={{ status: 'error', rows: [], error: 'Quota request failed' }} onRefresh={() => {}} />
<QuotaCard file={{ name: 'loading.json', provider: 'kimi' }} quota={{ status: 'loading', rows: [] }} onRefresh={() => {}} />
<QuotaCard file={{ name: 'idle.json', provider: 'kimi' }} quota={{ status: 'idle', rows: [] }} onRefresh={() => {}} />
</div>
</section>
</main>
</I18nProvider>,
);
+49
View File
@@ -0,0 +1,49 @@
// Run with the Vite dev server at QUOTA_TEST_BASE (default: http://127.0.0.1:1421).
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const base = process.env.QUOTA_TEST_BASE || 'http://127.0.0.1:1421';
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
try {
const page = await browser.newPage();
await page.route('**/*', route => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
await page.goto(`${base}/tests/fixtures/quota-layout.html`);
await page.locator('.real-quota-card').first().waitFor();
for (const [viewport, width, columns] of [[1920, 1600, 5], [1440, 1100, 3], [1440, 1000, 3], [1440, 924, 3], [1440, 923, 2], [1100, 800, 2], [1100, 612, 2], [1100, 611, 1], [640, 560, 1], [375, 320, 1], [320, 260, 1]]) {
await page.setViewportSize({ width: viewport, height: 1000 });
await page.locator('.content').evaluate((el, width) => { el.style.width = `${width}px`; }, width);
const layout = await page.evaluate(() => {
const cards = [...document.querySelectorAll('.real-quota-card')];
const rects = cards.map(card => card.getBoundingClientRect());
const issues = [];
for (const [index, card] of cards.entries()) {
const outer = rects[index];
const elements = [...card.querySelectorAll('.quota-identity, .quota-plan, .quota-status, .quota-row-list, .quota-card-actions, .quota-reset-footer, .quota-reset-credit-summary, .quota-card-message, .quota-card-error')];
for (const el of elements) {
const r = el.getBoundingClientRect();
if (r.left < outer.left - 1 || r.right > outer.right + 1 || r.bottom > outer.bottom + 1) issues.push(`${index}: ${el.className} outside card`);
}
for (let i = 0; i < elements.length; i++) for (let j = i + 1; j < elements.length; j++) {
const a = elements[i].getBoundingClientRect(), b = elements[j].getBoundingClientRect();
if (Math.min(a.right, b.right) - Math.max(a.left, b.left) > 1 && Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top) > 1) issues.push(`${index}: ${elements[i].className} overlaps ${elements[j].className}`);
}
}
return {
issues,
columns: rects.slice(0, 6).filter(r => Math.abs(r.top - rects[0].top) < 1).length,
quotaTexts: cards.slice(0, 2).map(card => card.querySelector('.quota-row-list').textContent),
};
});
const context = `viewport=${viewport}, content=${width}`;
assert.equal(layout.columns, columns, `${context}: column count adapts to available width`);
assert.deepEqual(layout.issues, [], context);
for (const text of layout.quotaTexts) {
assert.ok(text.includes('80%') && text.includes('60%'), `${context}: both Kimi windows remain rendered`);
}
}
console.log('Quota layout: 11 viewport/content combinations passed');
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });