Why: make material, background work, confirmations and files accessible to nontechnical users across desktop and mobile. Evidence: Dev SELF receipts for Frontend tsc, 190 tests, knip baseline, build, nine fixture browser cases and two real HTTP/browser smoke cases using MockRunner. Impact: existing API and permissions remain authoritative; stale events/queries and duplicate effects are guarded, while one file preview and reading state are preserved. Refs: work-companion-ui design r0.2 and plan r1 U2-U5; UI01-UI13.
53 lines
2.7 KiB
TypeScript
53 lines
2.7 KiB
TypeScript
// Project identity lives in context; the top bar keeps work/files reachable when panes close.
|
|
import { BrandMark, Icon, pendingCount } from '@ui'
|
|
import { store, useStore } from '../store'
|
|
import type { WorkbenchPane } from '../workbenchLayout'
|
|
|
|
interface TopBarProps {
|
|
compact?: boolean
|
|
activePane?: WorkbenchPane
|
|
showReturn?: boolean
|
|
onContext?: () => void
|
|
onConversation?: () => void
|
|
onWork?: () => void
|
|
onFiles?: () => void
|
|
}
|
|
|
|
export function TopBar({ compact = false, activePane = 'conversation', showReturn = false,
|
|
onContext = store.toggleLeft, onConversation,
|
|
onWork = () => { store.openInbox(); store.setUI({ inboxFilter: 'all', previewExpanded: false }) }, onFiles = store.openFiles,
|
|
}: TopBarProps = {}) {
|
|
const pending = useStore(s => pendingCount(s.cards) + pendingCount(s.projectCards))
|
|
const ui = useStore(s => s.ui)
|
|
const returnVisible = showReturn || compact && activePane !== 'conversation'
|
|
return (
|
|
<header className={`topbar companion-topbar${compact ? ' compact' : ''}`}>
|
|
<div className="topbar-identity"><BrandMark size={22} /><span className="topbar-name">工作助手</span></div>
|
|
<span className="topbar-spacer" />
|
|
{returnVisible && <button className="shell-return" aria-label="返回对话" onClick={onConversation}>
|
|
<Icon name="chevron-right" className="shell-back-icon" size={14} /><span>返回对话</span>
|
|
</button>}
|
|
<nav className="shell-navigation" aria-label="工作台导航">
|
|
{compact && <>
|
|
<button className="shell-nav-button" aria-label="打开上下文" aria-pressed={activePane === 'context'} onClick={onContext}>
|
|
<Icon name="panel-left" size={15} /><span>上下文</span>
|
|
</button>
|
|
<button className="shell-nav-button" aria-label="打开对话" aria-pressed={activePane === 'conversation'} onClick={onConversation}>
|
|
<Icon name="message" size={15} /><span>对话</span>
|
|
</button>
|
|
</>}
|
|
<button className="shell-nav-button" aria-label="打开工作"
|
|
aria-pressed={compact ? activePane === 'work' : !ui.rightCollapsed && ui.rightTab === 'work'}
|
|
title={pending > 0 ? `${pending} 项待处理` : '工作'} onClick={onWork}>
|
|
<Icon name="inbox" size={15} /><span>工作</span>
|
|
{pending > 0 && <span className="shell-pending" data-testid="topbar-pending">{pending > 99 ? '99+' : pending}</span>}
|
|
</button>
|
|
<button className="shell-nav-button" aria-label="打开文件"
|
|
aria-pressed={compact ? activePane === 'files' : !ui.rightCollapsed && ui.rightTab === 'files'} onClick={onFiles}>
|
|
<Icon name="folder" size={15} /><span>文件</span>
|
|
</button>
|
|
</nav>
|
|
</header>
|
|
)
|
|
}
|