From 2015d7670d20b0bdff7614d95b47ec314a79440b Mon Sep 17 00:00:00 2001 From: Will Date: Thu, 30 Jul 2026 11:34:08 +0800 Subject: [PATCH 1/2] fix(ranking): refine responsive toolbar layout Keep ranking filters compact across desktop and narrow cards while preserving full metric names in the dropdown. Align responsive profile actions with keyboard order and retain visible focus treatment inside the scrollable toolbar. --- web/src/components/ui/Select.tsx | 3 +- .../features/ranking/RankingPage.module.scss | 66 +++++++++++++------ web/src/features/ranking/RankingPage.tsx | 6 +- .../ranking/components/RankingToolbar.tsx | 27 ++++---- .../components/test/RankingToolbar.test.tsx | 22 +++++-- .../ranking/test/RankingPage.styles.test.ts | 23 +++++-- .../ranking/test/RankingPage.test.tsx | 10 +++ 7 files changed, 111 insertions(+), 46 deletions(-) diff --git a/web/src/components/ui/Select.tsx b/web/src/components/ui/Select.tsx index 1df569f9..dfb599a7 100644 --- a/web/src/components/ui/Select.tsx +++ b/web/src/components/ui/Select.tsx @@ -16,6 +16,7 @@ import styles from './Select.module.scss'; export interface SelectOption { value: string; label: string; + triggerLabel?: string; suffix?: ReactNode; suffixAriaLabel?: string; disabled?: boolean; @@ -198,7 +199,7 @@ export function Select({ ? selectedIndex : firstEnabledIndex; const selected = selectedIndex >= 0 ? options[selectedIndex] : undefined; - const displayText = selected?.label ?? placeholder ?? ''; + const displayText = selected?.triggerLabel ?? selected?.label ?? placeholder ?? ''; const isPlaceholder = !selected && placeholder; const commitSelection = useCallback( diff --git a/web/src/features/ranking/RankingPage.module.scss b/web/src/features/ranking/RankingPage.module.scss index 37da817d..dfee41a0 100644 --- a/web/src/features/ranking/RankingPage.module.scss +++ b/web/src/features/ranking/RankingPage.module.scss @@ -892,13 +892,12 @@ @mixin ranking-header-stacked { .leaderboardHeader { - grid-template-columns: minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: - 'title' - 'toolbar' - 'profile'; + 'title title' + 'toolbar profile'; align-items: stretch; - column-gap: 0; + column-gap: 12px; row-gap: 14px; padding: 0; } @@ -921,36 +920,43 @@ } .leaderboardHeaderActions { - justify-content: center; - justify-self: center; + align-self: center; + justify-content: flex-end; + justify-self: end; max-width: 100%; width: auto; margin-right: 0; } - .profileActionShell, - .profileActionShellActive { - max-width: 100%; - } - .toolbar { - display: grid; - grid-template-columns: minmax(0, 1fr); + display: flex; + justify-content: flex-start; width: 100%; + max-width: 100%; + padding: 4px; + overflow-x: auto; + overflow-y: hidden; + overscroll-behavior-inline: contain; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + + &::-webkit-scrollbar { + display: none; + } } .periods { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - width: 100%; - } - - .periodButton { - padding-inline: 6px; + grid-template-columns: repeat(4, max-content); + flex: 0 0 auto; + width: max-content; + margin-left: auto; } .metricControl { - width: 100%; + flex: 0 0 168px; + width: 168px; + margin-right: auto; } } @@ -961,6 +967,24 @@ @include mobile { @include ranking-header-stacked; + .profileActionShell { + max-width: 160px; + } + + .profileActionShellActive { + flex: 0 0 42px; + width: 42px; + } + + .profileActionShellActive :global(.main-action-button) { + width: 32px; + padding-inline: 5px; + } + + .profileActionName { + display: none; + } + .profileModal :global(.modal-body) { max-height: min(60dvh, calc(100dvh - 180px)); overflow: auto; diff --git a/web/src/features/ranking/RankingPage.tsx b/web/src/features/ranking/RankingPage.tsx index c6f70488..970c90b7 100644 --- a/web/src/features/ranking/RankingPage.tsx +++ b/web/src/features/ranking/RankingPage.tsx @@ -594,6 +594,7 @@ function LeaderboardCard({ const podium = rows.slice(0, 3); const tableRows = rows; const scoreExplanation = resolveScoreExplanation(board, metric, language); + const hasRankingProfile = status?.status === 'active' || status?.status === 'paused'; return (
@@ -644,12 +645,13 @@ function LeaderboardCard({ data-ranking-profile-action-shell > - {status?.status === 'active' || status?.status === 'paused' ? ( + {hasRankingProfile ? ( <> {status.display_name || t('ranking.profile_action')} diff --git a/web/src/features/ranking/components/RankingToolbar.tsx b/web/src/features/ranking/components/RankingToolbar.tsx index f142a76f..eb10175e 100644 --- a/web/src/features/ranking/components/RankingToolbar.tsx +++ b/web/src/features/ranking/components/RankingToolbar.tsx @@ -11,15 +11,16 @@ const PERIODS: ReadonlyArray<{ value: RankingPeriod; labelKey: string }> = [ { value: 'previous_month', labelKey: 'ranking.period_previous_month' }, ]; -const METRICS: ReadonlyArray<{ value: RankingMetric; labelKey: string }> = [ - { value: 'overall', labelKey: 'ranking.metric_overall' }, - { value: 'total_tokens', labelKey: 'ranking.metric_total_tokens' }, - { value: 'request_count', labelKey: 'ranking.metric_request_count' }, - { value: 'cache_read_rate', labelKey: 'ranking.metric_cache_read_rate' }, - { value: 'ttft_average', labelKey: 'ranking.metric_ttft_average' }, - { value: 'latency_average', labelKey: 'ranking.metric_latency_average' }, - { value: 'peak_tpm', labelKey: 'ranking.metric_peak_tpm' }, - { value: 'peak_rpm', labelKey: 'ranking.metric_peak_rpm' }, +// 触发器保持短名称,展开菜单保留完整指标语义。 +const METRICS: ReadonlyArray<{ value: RankingMetric; labelKey: string; triggerLabelKey: string }> = [ + { value: 'overall', labelKey: 'ranking.metric_overall', triggerLabelKey: 'ranking.metric_overall' }, + { value: 'total_tokens', labelKey: 'ranking.metric_total_tokens', triggerLabelKey: 'ranking.metric_short_total_tokens' }, + { value: 'request_count', labelKey: 'ranking.metric_request_count', triggerLabelKey: 'ranking.metric_short_request_count' }, + { value: 'cache_read_rate', labelKey: 'ranking.metric_cache_read_rate', triggerLabelKey: 'ranking.metric_short_cache_read_rate' }, + { value: 'ttft_average', labelKey: 'ranking.metric_ttft_average', triggerLabelKey: 'ranking.metric_short_ttft_average' }, + { value: 'latency_average', labelKey: 'ranking.metric_latency_average', triggerLabelKey: 'ranking.metric_short_latency_average' }, + { value: 'peak_tpm', labelKey: 'ranking.metric_peak_tpm', triggerLabelKey: 'ranking.metric_short_peak_tpm' }, + { value: 'peak_rpm', labelKey: 'ranking.metric_peak_rpm', triggerLabelKey: 'ranking.metric_short_peak_rpm' }, ]; export interface RankingToolbarProps { @@ -37,7 +38,11 @@ export function RankingToolbar({ }: RankingToolbarProps) { const { t } = useTranslation(); const metricOptions = useMemo( - () => METRICS.map((option) => ({ value: option.value, label: t(option.labelKey) })), + () => METRICS.map((option) => ({ + value: option.value, + label: t(option.labelKey), + triggerLabel: t(option.triggerLabelKey), + })), [t], ); @@ -64,7 +69,7 @@ export function RankingToolbar({