fix(desktop): improve activity summary layout

Use container queries for the activity summary grid so the Token usage panel responds to its own width instead of the full desktop viewport. This prevents loose medium-width layouts and over-compressed five-column cards when the settings page is shown with sidebars.

Tested:
- cd desktop && bun run test -- src/pages/ActivitySettings.test.tsx --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- cd desktop && bun run test -- src/theme/globals.test.ts --run --reporter=verbose --pool=forks --maxWorkers=1 --minWorkers=1
- bun run check:desktop
- Electron smoke at 1180px and 900px against the local dev server

Not-tested:
- Coverage report; this is a presentation-only desktop UI change.

Confidence: high
Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-06-03 18:28:17 +08:00
parent eaa940ab90
commit f567236330
4 changed files with 132 additions and 15 deletions

View File

@ -239,6 +239,30 @@ describe('ActivitySettings', () => {
expect(editButton.closest('div')).toHaveClass('group/activity-profile')
})
it('uses a balanced responsive summary grid instead of the loose medium-width layout', async () => {
render(<ActivitySettings />)
await flushActivityLoad()
const summaryPanel = screen.getByText('Total tokens').closest('section')
expect(summaryPanel).toHaveClass('activity-summary-panel')
const summaryGrid = summaryPanel?.querySelector('.activity-summary-grid')
expect(summaryGrid).not.toHaveClass('sm:grid-cols-2')
expect(summaryGrid).not.toHaveClass('lg:grid-cols-5')
expect(summaryGrid).not.toHaveClass('xl:grid-cols-5')
const primaryMetric = screen.getByText('Total tokens').closest('.activity-summary-metric')
expect(primaryMetric).toHaveClass('activity-summary-metric-primary')
expect(primaryMetric).not.toHaveClass('sm:col-span-2')
expect(primaryMetric).not.toHaveClass('lg:col-span-1')
const longestTaskValue = screen.getByText('0m')
expect(longestTaskValue).toHaveClass('activity-summary-value')
expect(longestTaskValue).not.toHaveClass('truncate')
expect(longestTaskValue).not.toHaveClass('break-words')
})
it('supports localized heatmap mode switches and persisted display name edits', async () => {
useSettingsStore.setState({ locale: 'zh' })
render(<ActivitySettings />)

View File

@ -608,26 +608,58 @@ export function ActivitySettings() {
{profileError && !isEditingProfile && <div className="mt-3 text-xs text-[var(--color-error)]">{profileError}</div>}
</section>
<section className="mx-auto mt-8 overflow-hidden rounded-[18px] border border-[var(--color-border)] bg-[var(--color-surface-container-lowest)]">
<section className="activity-summary-panel mx-auto mt-8 overflow-hidden rounded-[18px] border border-[var(--color-border)] bg-[var(--color-border)] p-px shadow-[0_18px_45px_-38px_rgba(15,23,42,0.65)]">
{isLoading ? (
<div className="grid gap-0 sm:grid-cols-2 xl:grid-cols-5">
<div className="activity-summary-grid grid gap-px">
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="h-[88px] animate-pulse border-t border-[var(--color-border)] bg-[var(--color-surface)] sm:border-l sm:border-t-0 first:sm:border-l-0" />
<div
key={index}
className={`activity-summary-metric min-h-[112px] animate-pulse bg-[var(--color-surface)] px-5 py-4 ${
index === 0 ? 'activity-summary-metric-primary' : ''
}`}
>
<div className="h-3 w-20 rounded bg-[var(--color-surface-container)]" />
<div className="mt-6 h-7 w-24 rounded bg-[var(--color-surface-container)]" />
<div className="mt-3 h-3 w-16 rounded bg-[var(--color-surface-container)]" />
</div>
))}
</div>
) : (
<div className="grid gap-0 sm:grid-cols-2 xl:grid-cols-5">
{metrics.map((metric, index) => (
<div
key={metric.label}
className="min-w-0 border-t border-[var(--color-border)] px-5 py-5 text-center opacity-0 [animation:activity-reveal_420ms_cubic-bezier(0.16,1,0.3,1)_forwards] sm:border-l sm:border-t-0 first:sm:border-l-0"
style={{ animationDelay: `${index * 45}ms` }}
>
<div className="truncate text-2xl font-semibold tracking-tight text-[var(--color-text-primary)]">{metric.value}</div>
<div className="mt-1 truncate text-sm font-semibold text-[var(--color-text-secondary)]">{metric.label}</div>
{metric.detail && <div className="mt-0.5 truncate text-xs text-[var(--color-text-tertiary)]">{metric.detail}</div>}
</div>
))}
<div className="activity-summary-grid grid gap-px">
{metrics.map((metric, index) => {
const isPrimary = index === 0
return (
<div
key={metric.label}
className={`activity-summary-metric min-w-0 bg-[var(--color-surface-container-lowest)] px-5 py-4 text-left opacity-0 shadow-[inset_0_1px_0_rgba(255,255,255,0.48)] [animation:activity-reveal_420ms_cubic-bezier(0.16,1,0.3,1)_forwards] ${
isPrimary ? 'activity-summary-metric-primary' : ''
}`}
style={{ animationDelay: `${index * 45}ms` }}
>
<div className="flex min-h-[104px] flex-col justify-between gap-4">
<div className="flex min-w-0 items-center gap-2">
<span
className={`h-1.5 w-1.5 shrink-0 rounded-full ${
isPrimary ? 'bg-[var(--color-primary)]' : 'bg-[var(--color-text-tertiary)] opacity-45'
}`}
aria-hidden="true"
/>
<div className="min-w-0 truncate text-[13px] font-semibold text-[var(--color-text-secondary)]">
{metric.label}
</div>
</div>
<div className="min-w-0">
<div className={`activity-summary-value max-w-full font-semibold leading-[1.08] tracking-tight text-[var(--color-text-primary)] tabular-nums ${
isPrimary ? 'text-[32px] lg:text-[26px]' : 'text-[28px] lg:text-[26px]'
}`}>
{metric.value}
</div>
{metric.detail && <div className="mt-2 truncate text-sm text-[var(--color-text-tertiary)]">{metric.detail}</div>}
</div>
</div>
</div>
)
})}
</div>
)}
</section>

View File

@ -1280,6 +1280,53 @@ button, input, textarea, select, a, [role="button"] {
background: var(--color-outline-a92);
}
.activity-summary-panel {
container-type: inline-size;
}
.activity-summary-grid {
grid-template-columns: minmax(0, 1fr);
}
.activity-summary-metric-primary {
grid-column: auto;
}
.activity-summary-value {
overflow-wrap: normal;
word-break: keep-all;
}
@container (min-width: 360px) {
.activity-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.activity-summary-metric-primary {
grid-column: span 2;
}
}
@container (min-width: 620px) {
.activity-summary-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.activity-summary-metric-primary {
grid-column: span 2;
}
}
@container (min-width: 860px) {
.activity-summary-grid {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.activity-summary-metric-primary {
grid-column: auto;
}
}
.activity-heat-cell {
position: relative;
transform: translateZ(0);

View File

@ -89,6 +89,20 @@ describe('desktop theme tokens', () => {
expect(css).toContain('box-shadow: var(--shadow-activity-cell-hover);')
})
it('uses container queries for the activity summary grid', () => {
const activitySummaryCss = getCssBetween('.activity-summary-panel {', '.activity-heat-cell {')
const mediumStart = activitySummaryCss.indexOf('@container (min-width: 620px)')
const wideStart = activitySummaryCss.indexOf('@container (min-width: 860px)')
const mediumSummaryCss = activitySummaryCss.slice(mediumStart, wideStart)
expect(activitySummaryCss).toContain('container-type: inline-size;')
expect(activitySummaryCss).toContain('@container (min-width: 360px)')
expect(activitySummaryCss).toContain('@container (min-width: 620px)')
expect(activitySummaryCss).toContain('@container (min-width: 860px)')
expect(activitySummaryCss).toContain('grid-template-columns: repeat(5, minmax(0, 1fr));')
expect(mediumSummaryCss).toContain('grid-column: span 2;')
})
it('avoids color-mix in the startup-critical UI zoom shell chrome for Safari 15 WebView support', () => {
const zoomShellCss = getCssBetween('.settings-zoom-kbd {', '/* ─── Tailwind Theme Override')