From b910f48da09ded3c3a012df69edac904619294ba Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:32:56 -0700 Subject: [PATCH] feat(status-bar): replace usage footer toggle with Compact/Detailed segmented control Swap the bottom-of-popover on/off switch for a SettingsSegmentedControl at the top of the Usage popover (view-switcher pattern), so both modes are named and discoverable on first open. Reuses the repo's canonical Compact/Detailed vocabulary from the Workspace card-layout control. --- .../status-bar/UsageRosterPanel.test.tsx | 99 +++++++++++++++---- .../status-bar/UsageRosterPanel.tsx | 50 +++++----- src/renderer/src/i18n/locales/en.json | 4 +- src/renderer/src/i18n/locales/es.json | 4 +- src/renderer/src/i18n/locales/ja.json | 4 +- src/renderer/src/i18n/locales/ko.json | 4 +- src/renderer/src/i18n/locales/zh.json | 4 +- 7 files changed, 122 insertions(+), 47 deletions(-) diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx index 2285e0c3737..c61faf88367 100644 --- a/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.test.tsx @@ -1,5 +1,9 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ProviderRateLimits } from '../../../../shared/rate-limit-types' const mocks = vi.hoisted(() => ({ @@ -17,21 +21,6 @@ vi.mock('@/hooks/useResetCountdownClock', () => ({ useResetCountdownClock: mocks.useResetCountdownClock })) vi.mock('@/components/ui/dropdown-menu', () => ({ - DropdownMenuCheckboxItem: ({ - children, - checked, - onCheckedChange: _onCheckedChange, - onSelect: _onSelect, - ...props - }: React.PropsWithChildren<{ - checked?: boolean - onCheckedChange?: (checked: boolean) => void - onSelect?: () => void - }>) => ( -
- {children} -
- ), DropdownMenuItem: ({ children, onSelect: _onSelect, @@ -137,7 +126,81 @@ describe('UsageRow', () => { expect(mocks.useResetCountdownClock).toHaveBeenCalledOnce() expect(mocks.useResetCountdownClock).toHaveBeenCalledWith([sessionReset, weeklyReset]) expect(markup).toContain('Resets in 2m') - expect(markup).toContain('Verbose footer') - expect(markup).toContain('data-checked="true"') + }) +}) + +describe('UsageRosterPanel density picker', () => { + let container: HTMLDivElement + let root: Root + + beforeEach(() => { + mocks.useResetCountdownClock.mockClear() + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + }) + + afterEach(() => { + act(() => { + root.unmount() + }) + container.remove() + }) + + function renderPanel( + statusBarUsageMode: 'verbose' | 'compact', + onStatusBarUsageModeChange: (mode: 'verbose' | 'compact') => void + ): void { + act(() => { + root.render( + {}} + onOpenProvider={() => {}} + onSignIn={() => {}} + canSignIn={() => true} + onManageAccounts={() => {}} + onUsageDetails={() => {}} + /> + ) + }) + } + + function segmentButton(label: string): HTMLButtonElement { + const button = [...container.querySelectorAll('button')].find( + (node) => node.textContent === label + ) + if (!button) { + throw new Error(`missing "${label}" segment`) + } + return button as HTMLButtonElement + } + + it('offers named Detailed/Compact segments and marks the active one', () => { + renderPanel('compact', () => {}) + + expect(container.textContent).toContain('Detailed') + expect(container.textContent).toContain('Compact') + expect(segmentButton('Compact').getAttribute('aria-checked')).toBe('true') + expect(segmentButton('Detailed').getAttribute('aria-checked')).toBe('false') + }) + + it('switches mode when a segment is chosen', () => { + const onStatusBarUsageModeChange = vi.fn() + renderPanel('compact', onStatusBarUsageModeChange) + + act(() => { + segmentButton('Detailed').click() + }) + expect(onStatusBarUsageModeChange).toHaveBeenLastCalledWith('verbose') + + act(() => { + segmentButton('Compact').click() + }) + expect(onStatusBarUsageModeChange).toHaveBeenLastCalledWith('compact') }) }) diff --git a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx index 5788e67969e..fa11825d140 100644 --- a/src/renderer/src/components/status-bar/UsageRosterPanel.tsx +++ b/src/renderer/src/components/status-bar/UsageRosterPanel.tsx @@ -1,6 +1,7 @@ import React from 'react' import { ChevronRight, RefreshCw } from 'lucide-react' -import { DropdownMenuCheckboxItem, DropdownMenuItem } from '@/components/ui/dropdown-menu' +import { DropdownMenuItem } from '@/components/ui/dropdown-menu' +import { SettingsSegmentedControl } from '@/components/settings/SettingsFormControls' import { useResetCountdownClock } from '@/hooks/useResetCountdownClock' import { translate } from '@/i18n/i18n' import { formatRateLimitWindowChipLabel, formatWindowLabel } from '@/lib/window-label-formatter' @@ -220,6 +221,30 @@ export function UsageRosterPanel({ + {/* Density picker lives at the top of the popover it controls (view-switcher + pattern) so both modes are named and discoverable on first open. */} +
+ + value={statusBarUsageMode} + onChange={onStatusBarUsageModeChange} + ariaLabel={translate( + 'auto.components.status.bar.UsageRosterPanel.footerDetailAria', + 'Usage footer detail' + )} + size="sm" + equalWidth + options={[ + { + value: 'verbose', + label: translate('auto.components.status.bar.UsageRosterPanel.detailed', 'Detailed') + }, + { + value: 'compact', + label: translate('auto.components.status.bar.UsageRosterPanel.compact', 'Compact') + } + ]} + /> +
{sorted.map((p) => { const state = getUsageRosterRowState(p, usedSections(p).length > 0) @@ -259,29 +284,6 @@ export function UsageRosterPanel({ ) })}
- { - onStatusBarUsageModeChange(checked ? 'verbose' : 'compact') - }} - onSelect={(event) => event.preventDefault()} - className="w-full cursor-pointer justify-between rounded-none px-3.5 py-2.5 text-[13px] text-foreground [&>span:first-child]:hidden" - > - {translate('auto.components.status.bar.UsageRosterPanel.verboseFooter', 'Verbose footer')} - {/* Why: one menu control avoids nesting the SettingsSwitch button inside an interactive item. */} - - - -