mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
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.
This commit is contained in:
@@ -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
|
||||
}>) => (
|
||||
<div data-checked={checked} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
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(
|
||||
<UsageRosterPanel
|
||||
providers={[]}
|
||||
display="used"
|
||||
statusBarUsageMode={statusBarUsageMode}
|
||||
onStatusBarUsageModeChange={onStatusBarUsageModeChange}
|
||||
isRefreshing={false}
|
||||
onRefresh={() => {}}
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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({
|
||||
</DropdownMenuItem>
|
||||
</div>
|
||||
</div>
|
||||
{/* Density picker lives at the top of the popover it controls (view-switcher
|
||||
pattern) so both modes are named and discoverable on first open. */}
|
||||
<div className="px-3.5 pb-2.5">
|
||||
<SettingsSegmentedControl<StatusBarUsageMode>
|
||||
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')
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="border-t border-border/70" />
|
||||
{sorted.map((p) => {
|
||||
const state = getUsageRosterRowState(p, usedSections(p).length > 0)
|
||||
@@ -259,29 +284,6 @@ export function UsageRosterPanel({
|
||||
)
|
||||
})}
|
||||
<div className="border-t border-border/70" />
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={statusBarUsageMode === 'verbose'}
|
||||
onCheckedChange={(checked) => {
|
||||
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. */}
|
||||
<span
|
||||
aria-hidden
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border border-transparent transition-colors ${
|
||||
statusBarUsageMode === 'verbose' ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
statusBarUsageMode === 'verbose' ? 'translate-x-4' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={onUsageDetails}
|
||||
className="w-full cursor-pointer justify-between rounded-none px-3.5 py-2.5 text-[13px] text-foreground"
|
||||
|
||||
@@ -3309,7 +3309,9 @@
|
||||
"loadingUsage": "Loading usage…",
|
||||
"usageUnavailable": "Usage unavailable",
|
||||
"noUsageData": "No usage data",
|
||||
"verboseFooter": "Verbose footer"
|
||||
"detailed": "Detailed",
|
||||
"compact": "Compact",
|
||||
"footerDetailAria": "Usage footer detail"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3286,7 +3286,9 @@
|
||||
"loadingUsage": "Loading usage…",
|
||||
"usageUnavailable": "Usage unavailable",
|
||||
"noUsageData": "No usage data",
|
||||
"verboseFooter": "Verbose footer"
|
||||
"detailed": "Detailed",
|
||||
"compact": "Compact",
|
||||
"footerDetailAria": "Usage footer detail"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3286,7 +3286,9 @@
|
||||
"loadingUsage": "Loading usage…",
|
||||
"usageUnavailable": "Usage unavailable",
|
||||
"noUsageData": "No usage data",
|
||||
"verboseFooter": "Verbose footer"
|
||||
"detailed": "Detailed",
|
||||
"compact": "Compact",
|
||||
"footerDetailAria": "Usage footer detail"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3286,7 +3286,9 @@
|
||||
"loadingUsage": "Loading usage…",
|
||||
"usageUnavailable": "Usage unavailable",
|
||||
"noUsageData": "No usage data",
|
||||
"verboseFooter": "Verbose footer"
|
||||
"detailed": "Detailed",
|
||||
"compact": "Compact",
|
||||
"footerDetailAria": "Usage footer detail"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3286,7 +3286,9 @@
|
||||
"loadingUsage": "Loading usage…",
|
||||
"usageUnavailable": "Usage unavailable",
|
||||
"noUsageData": "No usage data",
|
||||
"verboseFooter": "Verbose footer"
|
||||
"detailed": "Detailed",
|
||||
"compact": "Compact",
|
||||
"footerDetailAria": "Usage footer detail"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user