mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-08-19 13:33:23 +00:00
fix(ui): prevent responsive panels from clipping content
This commit is contained in:
parent
4a152d959c
commit
440241faa0
5 changed files with 94 additions and 13 deletions
|
|
@ -2374,7 +2374,14 @@ function ProviderAccountsOverview({
|
|||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("grid h-full min-h-0 grid-cols-1 overflow-y-auto pr-1", providerAccountGapClass(dimensions), providerAccountGridClass(dimensions, visibleAccounts.length))}>
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-full min-h-0 auto-rows-max content-start grid-cols-1 overflow-y-auto pb-2 pr-2 [scrollbar-gutter:stable]",
|
||||
providerAccountGapClass(dimensions),
|
||||
providerAccountGridClass(dimensions, visibleAccounts.length)
|
||||
)}
|
||||
data-provider-account-grid="true"
|
||||
>
|
||||
{visibleAccounts.map((account) => {
|
||||
return <ProviderAccountSummaryCard account={account} dimensions={dimensions} key={providerAccountSnapshotKey(account)} refreshing={refreshing} variant={variant} onRefresh={onRefresh} />;
|
||||
})}
|
||||
|
|
@ -2406,7 +2413,7 @@ function ProviderAccountSinglePanel({
|
|||
|
||||
return (
|
||||
<div className={cn("flex h-full min-h-0 min-w-0 flex-col overflow-hidden", providerAccountStackClass(dimensions))}>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="flex min-w-0 shrink-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className={cn("truncate font-semibold", dimensions.height <= 1 ? "text-[12px]" : "text-[13px]")}>{providerAccountSnapshotLabel(account)}</div>
|
||||
{providerAccountShowRefreshTime(dimensions) ? <div className="mt-0.5 truncate text-[11px] text-muted-foreground">{formatProviderAccountRefreshTime(account, t)}</div> : null}
|
||||
|
|
@ -2453,7 +2460,7 @@ function ProviderAccountSummaryCard({
|
|||
const showQuotaVisual = providerAccountUsesQuotaVisual(variant) && quotaMeters.length > 0;
|
||||
|
||||
return (
|
||||
<div className={cn("overview-nested-surface min-h-0 min-w-0 overflow-hidden border", providerAccountCardPaddingClass(dimensions))}>
|
||||
<div className={cn("overview-nested-surface h-fit min-w-0 self-start overflow-hidden border", providerAccountCardPaddingClass(dimensions))}>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[13px] font-semibold">{providerAccountSnapshotLabel(account)}</div>
|
||||
|
|
@ -2470,7 +2477,7 @@ function ProviderAccountSummaryCard({
|
|||
<ProviderAccountBalanceMetric dimensions={dimensions} meter={balanceMeter} compact />
|
||||
</div>
|
||||
) : meters.length > 0 ? (
|
||||
<div className={cn("mt-2 min-h-0 overflow-hidden", providerAccountStackClass(dimensions))}>
|
||||
<div className={cn("mt-2 min-h-0", providerAccountStackClass(dimensions))}>
|
||||
{meters.map((meter) => (
|
||||
<ProviderAccountMeterLine account={account} dimensions={dimensions} key={meter.id} meter={meter} onRefresh={onRefresh} />
|
||||
))}
|
||||
|
|
@ -2563,7 +2570,7 @@ function ProviderAccountMeterLine({
|
|||
) : null}
|
||||
<div className={titleClassName}>{title}</div>
|
||||
</div>
|
||||
<div className={valueClassName}>{formatProviderAccountMeterValue(meter)}</div>
|
||||
<div className={valueClassName}>{formatProviderAccountMeterValue(meter, t)}</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ export function MainLayout({
|
|||
}}
|
||||
aria-hidden={!sidebarOpen}
|
||||
className={cn(
|
||||
"app-sidebar flex shrink-0 flex-col overflow-hidden bg-sidebar/95 max-[720px]:h-auto",
|
||||
"app-sidebar flex min-h-0 shrink-0 flex-col overflow-hidden bg-sidebar/95 max-[720px]:h-auto",
|
||||
sidebarOpen && compactLayout && "border-b border-border"
|
||||
)}
|
||||
id="primary-sidebar"
|
||||
|
|
@ -217,7 +217,7 @@ export function MainLayout({
|
|||
<div className="app-drag min-w-0 flex-1" />
|
||||
</div>
|
||||
|
||||
<nav className="flex min-h-0 flex-1 flex-col gap-4 px-2 py-3 max-[720px]:flex-none max-[720px]:flex-row max-[720px]:gap-1 max-[720px]:overflow-x-auto max-[720px]:py-2" aria-label={copy.sidebar.primaryNavigation}>
|
||||
<nav className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto px-2 py-3 max-[720px]:flex-none max-[720px]:flex-row max-[720px]:gap-1 max-[720px]:overflow-x-auto max-[720px]:overflow-y-hidden max-[720px]:py-2" aria-label={copy.sidebar.primaryNavigation}>
|
||||
{navigationGroups.map((group) => (
|
||||
<div className="grid min-w-0 gap-1 max-[720px]:contents" key={group.id}>
|
||||
<div className="px-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/65 max-[720px]:hidden">
|
||||
|
|
|
|||
|
|
@ -145,7 +145,10 @@ export function providerAccountProgressClass(status: ProviderAccountSnapshot["st
|
|||
return "bg-emerald-500";
|
||||
}
|
||||
|
||||
export function formatProviderAccountMeterValue(meter: ProviderAccountMeter): string {
|
||||
export function formatProviderAccountMeterValue(
|
||||
meter: ProviderAccountMeter,
|
||||
translate: (value: string) => string = (value) => value
|
||||
): string {
|
||||
const value = meter.remaining ?? meter.used ?? meter.limit;
|
||||
if (value === undefined) {
|
||||
return "-";
|
||||
|
|
@ -170,10 +173,11 @@ export function formatProviderAccountMeterValue(meter: ProviderAccountMeter): st
|
|||
if (unit === "minutes") {
|
||||
return `${formatProviderAccountNumber(value)}m`;
|
||||
}
|
||||
const displayUnit = translate(unit);
|
||||
if (meter.kind === "balance") {
|
||||
return `${formatProviderAccountNumber(value)} ${unit}`;
|
||||
return `${formatProviderAccountNumber(value)} ${displayUnit}`;
|
||||
}
|
||||
return `${formatCompactNumber(value)} ${unit}`;
|
||||
return `${formatCompactNumber(value)} ${displayUnit}`;
|
||||
}
|
||||
|
||||
export function formatProviderAccountNumber(value: number): string {
|
||||
|
|
|
|||
|
|
@ -2,12 +2,12 @@ import assert from "node:assert/strict";
|
|||
import test from "node:test";
|
||||
import * as React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { GatewayStartupErrorBanner, groupSidebarNavigation, UpdateEntryButton } from "@ccr/ui/pages/home/components/layout.tsx";
|
||||
import { GatewayStartupErrorBanner, groupSidebarNavigation, MainLayout, UpdateEntryButton } from "@ccr/ui/pages/home/components/layout.tsx";
|
||||
import { MediaModelConfigurationPanel, VirtualModelsView } from "@ccr/ui/pages/home/components/virtual-models.tsx";
|
||||
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
|
||||
import { createVirtualModelDraft } from "@ccr/ui/pages/home/shared/virtual-models.ts";
|
||||
import { appConfigFixture } from "../fixtures/index.ts";
|
||||
import { fallbackUpdateStatus } from "@ccr/ui/pages/home/shared/fallbacks.ts";
|
||||
import { fallbackGatewayStatus, fallbackUpdateStatus } from "@ccr/ui/pages/home/shared/fallbacks.ts";
|
||||
import { navigation } from "@ccr/ui/pages/home/shared/options.ts";
|
||||
import { shouldCheckForUpdateOnOpen } from "@ccr/ui/pages/home/components/update.tsx";
|
||||
|
||||
|
|
@ -32,6 +32,41 @@ test("sidebar navigation groups pages and hides networking from the sidebar", ()
|
|||
]);
|
||||
});
|
||||
|
||||
test("sidebar navigation scrolls vertically without displacing the settings footer", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<MainLayout
|
||||
activeView="networking"
|
||||
agentAnalysisEnabled={false}
|
||||
compactLayout={false}
|
||||
config={appConfigFixture()}
|
||||
copy={appCopy.en}
|
||||
gatewayActionBusy={false}
|
||||
gatewayEndpoint="http://127.0.0.1:3456"
|
||||
gatewayStatus={fallbackGatewayStatus}
|
||||
isMac={false}
|
||||
needsTrafficLightSafeArea={false}
|
||||
networkCaptureEnabled={false}
|
||||
onOpenServerSettings={() => undefined}
|
||||
onOpenSettings={() => undefined}
|
||||
onOpenUpdate={() => undefined}
|
||||
onSelectNavigationItem={() => undefined}
|
||||
onToggleSidebar={() => undefined}
|
||||
requestLogsEnabled={false}
|
||||
shouldReduceMotion={true}
|
||||
sidebarOpen
|
||||
toggleGatewayService={() => undefined}
|
||||
updateActionBusy={false}
|
||||
updateStatus={fallbackUpdateStatus}
|
||||
viewProps={{} as never}
|
||||
visibleNavigation={navigation}
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /<nav class="[^"]*overflow-y-auto[^"]*max-\[720px\]:overflow-y-hidden[^"]*"/);
|
||||
assert.match(html, /class="grid shrink-0 gap-1 border-t/);
|
||||
assert.ok(html.indexOf("</nav>") < html.indexOf("Settings"));
|
||||
});
|
||||
|
||||
test("GatewayStartupErrorBanner renders startup failure details", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AppI18nContext.Provider value={appCopy.zh}>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { renderToStaticMarkup } from "react-dom/server";
|
|||
import { formatCodexResetCardExpiry, formatCodexResetCardNumber, OverviewView } from "@ccr/ui/pages/home/components/dashboard.tsx";
|
||||
import { AppI18nContext, appCopy } from "@ccr/ui/pages/home/shared/i18n.tsx";
|
||||
import { parseStatusBucketDate } from "@ccr/ui/pages/home/shared/controls.tsx";
|
||||
import { providerAccountMeterDetailValidityProgress } from "@ccr/ui/pages/home/shared/provider-accounts.ts";
|
||||
import { formatProviderAccountMeterValue, providerAccountMeterDetailValidityProgress } from "@ccr/ui/pages/home/shared/provider-accounts.ts";
|
||||
import type { OverviewWidgetConfig, ProviderAccountSnapshot } from "@ccr/core/contracts/app.ts";
|
||||
import { accountSnapshots, installBrowserGlobals, usageStats } from "../fixtures/index.ts";
|
||||
|
||||
|
|
@ -143,6 +143,26 @@ test("OverviewView renders the empty widget layout state", () => {
|
|||
assert.match(html, /aria-label="Edit widgets"/);
|
||||
});
|
||||
|
||||
test("OverviewView keeps multi-provider account cards at their content height", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<OverviewView
|
||||
overviewWidgets={[{ enabled: true, id: "account", size: "4:2", type: "account-balance", variant: "cards" }]}
|
||||
providerAccounts={accountSnapshots()}
|
||||
refreshProviderAccounts={() => undefined}
|
||||
setUsageRange={() => undefined}
|
||||
usageRange="30d"
|
||||
usageStats={usageStats("30d")}
|
||||
onWidgetsChange={() => undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
assert.match(html, /data-provider-account-grid="true"/);
|
||||
assert.match(html, /auto-rows-max/);
|
||||
assert.match(html, /content-start/);
|
||||
assert.match(html, /scrollbar-gutter:stable/);
|
||||
assert.match(html, /h-fit/);
|
||||
});
|
||||
|
||||
test("OverviewView prioritizes Codex manual resets before folded balance meters", () => {
|
||||
const resetAt = new Date(Date.now() + 72 * 60 * 60 * 1000).toISOString();
|
||||
const resetEffectiveAt = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
|
|
@ -282,6 +302,21 @@ test("OverviewView does not render an outer progress bar for Codex manual resets
|
|||
assert.doesNotMatch(html, /Full reset/);
|
||||
});
|
||||
|
||||
test("provider account meter values localize textual units", () => {
|
||||
const value = formatProviderAccountMeterValue(
|
||||
{
|
||||
id: "codex_manual_resets",
|
||||
kind: "requests",
|
||||
label: "Manual resets",
|
||||
remaining: 0,
|
||||
unit: "resets"
|
||||
},
|
||||
(unit) => appCopy.zh.text[unit] ?? unit
|
||||
);
|
||||
|
||||
assert.equal(value, `0 ${appCopy.zh.text.resets}`);
|
||||
});
|
||||
|
||||
test("provider account reset credit detail progress uses each validity window", () => {
|
||||
const effectiveAt = "2026-07-01T00:00:00.000Z";
|
||||
const expiresAt = "2026-07-11T00:00:00.000Z";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue