diff --git a/src/pages/QuotaPage.css b/src/pages/QuotaPage.css index 1bd1d20..b5545c5 100644 --- a/src/pages/QuotaPage.css +++ b/src/pages/QuotaPage.css @@ -1,99 +1,66 @@ -.quota-page .quota-group-heading { - justify-content: flex-start; - min-height: 32px; - padding: 0 2px; -} - -.quota-page .quota-provider-title { - min-width: 0; - flex-wrap: wrap; - gap: 10px; -} - +.quota-page .quota-group-list { gap: 18px; } +.quota-page .quota-provider-group { gap: 8px; } +.quota-page .quota-group-heading { justify-content: flex-start; min-height: 24px; padding: 0 2px; } +.quota-page .quota-provider-title { min-width: 0; flex-wrap: wrap; gap: 8px; } +.quota-page .quota-group-heading h2 { font-size: var(--font-size-label); } +.quota-page .quota-group-heading .provider-logo { width: 18px; height: 18px; } .quota-page .quota-provider-count { - padding: 3px 8px; + padding: 2px 7px; border: 1px solid var(--border-color); border-radius: 6px; - background: var(--bg-card); color: var(--text-tertiary); - font-size: var(--font-size-caption); - font-weight: var(--font-weight-medium); - line-height: 1.4; + font-size: 11px; font-variant-numeric: tabular-nums; } -/* Dense account list inspired by the quota overview reference. */ -.quota-page .quota-group-list { gap: 14px; } -.quota-page .quota-table-head { +/* Keep each card compact; available content width determines the column count. */ +.quota-page .real-quota-grid { display: grid; - grid-template-columns: minmax(220px, 1.35fr) minmax(100px, .65fr) minmax(110px, .7fr) minmax(280px, 2fr) minmax(112px, .7fr); - align-items: center; - gap: 18px; - padding: 0 18px 6px; - color: var(--theme-7a746c); - font-size: var(--font-size-meta); - font-weight: var(--font-weight-semibold); + grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr)); + gap: 12px; + align-items: stretch; } -.quota-page .quota-provider-group { gap: 7px; } -.quota-page .quota-group-heading { min-height: 24px; padding: 0 3px; } -.quota-page .quota-group-heading h2 { font-size: var(--font-size-label); color: var(--theme-7a746c); } -.quota-page .quota-group-heading .provider-logo { width: 18px; height: 18px; } -.quota-page .quota-provider-count { padding: 2px 7px; font-size: 11px; } -.quota-page .real-quota-grid { display: grid; gap: 8px; } .quota-page .real-quota-card { - display: grid; - grid-template-columns: minmax(220px, 1.35fr) minmax(100px, .65fr) minmax(110px, .7fr) minmax(280px, 2fr) minmax(112px, .7fr); - align-items: center; - gap: 14px 18px; - min-height: 104px; - padding: 14px 18px; + display: flex; + flex-direction: column; + gap: 12px; + min-width: 0; + padding: 14px; border: 1px solid var(--border-color); border-radius: 9px; background: var(--bg-card); box-shadow: 0 1px 2px rgb(40 45 55 / 3%); } .quota-page .real-quota-card:hover { border-color: #b7c9ed; box-shadow: 0 3px 12px rgb(55 95 180 / 8%); } -.quota-page .real-quota-card-header { display: contents; } -.quota-page .quota-identity { display: flex; align-items: center; gap: 10px; min-width: 0; } -.quota-page .quota-identity > div { display: grid; gap: 4px; min-width: 0; } -.quota-page .quota-account-icon { width: 30px; height: 30px; flex: 0 0 auto; } +.quota-page .real-quota-card-header { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; +} +.quota-page .real-quota-card-header .quota-identity { display: flex; align-items: center; gap: 8px; min-width: 0; } +.quota-page .quota-identity > div { display: grid; gap: 3px; min-width: 0; } +.quota-page .quota-account-icon { width: 26px; height: 26px; flex: 0 0 auto; } .quota-page .real-quota-card-header strong { color: var(--theme-2d2a26); font-size: var(--font-size-body); } .quota-page .real-quota-card-header span { font-size: var(--font-size-meta); } -.quota-page .quota-plan, .quota-page .quota-status { display: grid; gap: 4px; min-width: 0; } -.quota-page .quota-plan strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.quota-page .quota-plan span, .quota-page .quota-status small { color: var(--theme-7a746c); font-size: var(--font-size-meta); } -.quota-page .quota-status-badge { width: fit-content; padding: 3px 8px; border-radius: 6px; background: var(--theme-f4f1ea); color: var(--theme-7a746c); font-size: var(--font-size-meta); font-weight: var(--font-weight-semibold); } +.quota-page .quota-plan { grid-column: 1; grid-row: 2; } +.quota-page .quota-status { grid-column: 2; grid-row: 2; justify-items: end; } +.quota-page .quota-plan, .quota-page .quota-status { display: grid; gap: 3px; min-width: 0; } +.quota-page .quota-plan span, .quota-page .quota-status small { color: var(--text-tertiary); font-size: var(--font-size-meta); } +.quota-page .quota-status-badge { width: fit-content; padding: 3px 7px; border-radius: 6px; background: var(--theme-f4f1ea); color: var(--theme-7a746c); font-size: var(--font-size-meta); font-weight: var(--font-weight-semibold); } .quota-page .quota-status-badge.ready { background: #e8f8ee; color: #149447; } .quota-page .quota-status-badge.error { background: #fff0ee; color: #d94b42; } -.quota-page .quota-row-list { grid-column: 4; gap: 9px; min-width: 0; } -.quota-page .quota-row-list { grid-row: 1 / span 2; align-self: center; } -.quota-page .real-quota-row { gap: 5px; } -.quota-page .real-quota-row > div:first-child { font-size: var(--font-size-meta); } -.quota-page .real-quota-track { height: 7px; background: #edf0f4; } +.quota-page .quota-card-actions { grid-column: 2; grid-row: 1; justify-content: flex-end; } +.quota-page .quota-card-actions .icon-button { width: 30px; height: 30px; border-radius: 7px; background: #eef4ff; color: #347df4; } +.quota-page .quota-row-list { order: 1; gap: 10px; min-width: 0; } +.quota-page .real-quota-row { gap: 4px; min-width: 0; overflow-wrap: anywhere; } +.quota-page .real-quota-row > div:first-child { font-size: var(--font-size-meta); align-items: flex-start; } +.quota-page .real-quota-row > div:first-child strong { flex-shrink: 0; } +.quota-page .real-quota-track { height: 6px; background: #edf0f4; } .quota-page .real-quota-track span { background: #20bf62; } .quota-page .real-quota-row small { font-size: 11px; } -.quota-page .quota-card-actions { grid-column: 5; grid-row: 1; justify-content: flex-end; align-self: center; flex-wrap: wrap; } -.quota-page .quota-reset-footer { grid-column: 5; grid-row: 2; justify-self: end; align-self: end; margin: 0; min-height: 30px; padding: 5px 10px; border-radius: 7px; color: #347df4; border-color: #d8e5fb; background: #f3f7ff; } +.quota-page .quota-reset-credit-summary { order: 2; gap: 5px 10px; padding: 8px; } +.quota-page .quota-reset-credit-summary span { white-space: normal; overflow-wrap: anywhere; } +.quota-page .quota-reset-footer { order: 3; align-self: flex-end; margin: auto 0 0; min-height: 30px; padding: 5px 10px; border-radius: 7px; color: #347df4; border-color: #d8e5fb; background: #f3f7ff; } .quota-page .quota-reset-footer:hover:not(:disabled) { border-color: #9dbcf4; background: #eaf2ff; } -.quota-page .quota-card-actions .icon-button { width: 30px; height: 30px; border-radius: 7px; background: #eef4ff; color: #347df4; } -.quota-page .quota-card-message, .quota-page .quota-card-error, .quota-page .quota-action-feedback { grid-column: 1 / -1; } -.quota-page .quota-reset-credit-summary { grid-column: 1 / 4; grid-row: 2; align-self: end; } -.quota-page .quota-card-message { min-height: 36px; } -.quota-page .quota-card-error { min-height: 36px; } - -@media (max-width: 980px) { - .quota-page .quota-table-head { display: none; } - .quota-page .real-quota-card { grid-template-columns: minmax(180px, 1fr) minmax(100px, .7fr) minmax(100px, .7fr) 38px; } - .quota-page .quota-row-list { grid-column: 1 / -1; grid-row: auto; } - .quota-page .quota-reset-credit-summary { grid-column: 1 / -1; grid-row: auto; } - .quota-page .quota-card-actions { grid-column: 4; } - .quota-page .quota-reset-footer { grid-column: 4; grid-row: 2; } -} -@media (max-width: 640px) { - .quota-page .real-quota-card { grid-template-columns: 1fr auto; padding: 13px; } - .quota-page .quota-plan, .quota-page .quota-status { grid-column: 1 / -1; } - .quota-page .quota-card-actions { grid-column: 2; grid-row: 1; } - .quota-page .quota-reset-footer { grid-column: 2; grid-row: 2; } - .quota-page .quota-row-list { grid-column: 1 / -1; grid-row: auto; } - .quota-page .quota-reset-credit-summary { grid-column: 1 / -1; grid-row: auto; } -} +.quota-page .quota-card-message, .quota-page .quota-card-error { min-height: 36px; flex-wrap: wrap; } diff --git a/src/pages/QuotaPage.tsx b/src/pages/QuotaPage.tsx index 43adb3d..9b24f26 100644 --- a/src/pages/QuotaPage.tsx +++ b/src/pages/QuotaPage.tsx @@ -143,11 +143,8 @@ export function QuotaPage() { }, [files, quotas]); return ( -
+
{confirmationDialog} -
-

{t('quota.title')}

-
{error ? : null} } @@ -166,7 +163,6 @@ export function QuotaPage() {
{t('quota.empty.title')}{t('quota.empty.description')}
) : (
- {grouped.map(([provider, items]) => (
diff --git a/tests/fixtures/quota-layout.html b/tests/fixtures/quota-layout.html new file mode 100644 index 0000000..c232e16 --- /dev/null +++ b/tests/fixtures/quota-layout.html @@ -0,0 +1 @@ +
diff --git a/tests/fixtures/quota-layout.tsx b/tests/fixtures/quota-layout.tsx new file mode 100644 index 0000000..94092aa --- /dev/null +++ b/tests/fixtures/quota-layout.tsx @@ -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( + +
+
+
+ {[1, 2, 3, 4, 5, 6].map((id) => {}} />)} +
+
+ {}} onReset={() => {}} /> + {}} /> + {}} /> + {}} /> +
+
+
+
, +); diff --git a/tests/quota-layout-ui.cjs b/tests/quota-layout-ui.cjs new file mode 100644 index 0000000..4a78ab4 --- /dev/null +++ b/tests/quota-layout-ui.cjs @@ -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; });