mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
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:
parent
eaa940ab90
commit
f567236330
@ -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 />)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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')
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user