Files
resolutionflow/frontend/src/components/layout/TopBar.tsx
chihlasm 2bcd3e2f3c fix: design system v4 polish — home icon, mobile hamburger, contrast, font-label cleanup
- Home sidebar icon: always cyan, bg-accent-dim only when route is "/"
- Mobile TopBar: add left padding so hamburger isn't hidden behind logo
- Landing page: bump card border color (#1e2130 → #2a2f3d) for better contrast
- Replace all font-label references (40 occurrences, 19 files) with font-mono or font-sans
- Remove deprecated --font-label CSS variable from index.css
- Convert hardcoded hex in layout inline styles to CSS variables (light-mode ready)
- Add @types/react-syntax-highlighter for script builder types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:19:44 +00:00

199 lines
8.0 KiB
TypeScript

import { useState, useRef, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Search, Zap, LogOut, Shield, Settings, HelpCircle } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
import { usePermissions } from '@/hooks/usePermissions'
import { BrandLogo } from '@/components/common/BrandLogo'
import { CommandPalette } from './CommandPalette'
import { QuickLaunch } from './QuickLaunch'
import { NotificationsPanel } from './NotificationsPanel'
import { cn } from '@/lib/utils'
export function TopBar() {
const navigate = useNavigate()
const { user, logout } = useAuthStore()
const { effectiveRole, isSuperAdmin } = usePermissions()
const [userMenuOpen, setUserMenuOpen] = useState(false)
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
const [quickLaunchOpen, setQuickLaunchOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const handleLogout = async () => {
setUserMenuOpen(false)
await logout()
navigate('/login')
}
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setUserMenuOpen(false)
}
}
if (userMenuOpen) document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [userMenuOpen])
// Cmd+K / Ctrl+K global shortcut
const handleGlobalKeyDown = useCallback((e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
setCommandPaletteOpen(prev => !prev)
}
}, [])
useEffect(() => {
document.addEventListener('keydown', handleGlobalKeyDown)
return () => document.removeEventListener('keydown', handleGlobalKeyDown)
}, [handleGlobalKeyDown])
const initials = user?.name
? user.name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)
: user?.email?.[0]?.toUpperCase() || '?'
return (
<>
<header
className="topbar relative z-10 flex items-center gap-4 px-4 pl-14 md:pl-4"
style={{
background: 'var(--color-bg-sidebar)',
borderBottom: '1px solid var(--color-border-default)',
}}
>
{/* Logo area */}
<Link
to="/"
className="flex items-center gap-2.5 pr-4 transition-all duration-200"
>
<BrandLogo size="sm" />
<span className="text-sm font-heading font-bold tracking-tight whitespace-nowrap text-text-heading">
ResolutionFlow
</span>
</Link>
{/* Spacer - push search to center */}
<div className="flex-1" />
{/* Search trigger — icon on mobile, full bar on desktop */}
<button
onClick={() => setCommandPaletteOpen(true)}
className="hidden sm:relative sm:block w-full text-left"
style={{ maxWidth: '480px' }}
>
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<div
className="w-full rounded-md py-2 pl-9 pr-14 text-[0.8125rem] text-muted-foreground cursor-pointer transition-colors"
style={{
background: 'var(--color-bg-card)',
border: '1px solid var(--color-border-default)',
}}
onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border-hover)' }}
onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.borderColor = 'var(--color-border-default)' }}
>
Search flows, sessions, tags...
</div>
<span
className="absolute right-3 top-1/2 -translate-y-1/2 rounded px-1.5 py-0.5 font-mono text-[0.625rem] text-text-muted"
style={{ background: 'var(--color-bg-page)', border: '1px solid var(--color-border-default)' }}
>
{navigator.platform?.toLowerCase().includes('mac') ? '\u2318K' : 'Ctrl+K'}
</span>
</button>
<button
onClick={() => setCommandPaletteOpen(true)}
className="sm:hidden rounded-lg p-2 text-muted-foreground hover:text-foreground transition-colors"
title="Search"
>
<Search size={18} />
</button>
{/* Spacer - push actions to right */}
<div className="flex-1" />
{/* Action buttons */}
<div className="flex items-center gap-1">
<button
onClick={() => setQuickLaunchOpen(true)}
className="rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors"
title="Quick Launch"
>
<Zap size={18} />
</button>
<Link
to="/guides"
className="rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors"
title="User Guides"
>
<HelpCircle size={18} />
</Link>
<NotificationsPanel />
{/* User avatar & menu */}
<div className="relative ml-2" ref={menuRef}>
<button
onClick={() => setUserMenuOpen(!userMenuOpen)}
className="flex h-8 w-8 items-center justify-center rounded-[10px] text-xs font-heading font-bold text-white hover:opacity-90 transition-opacity"
style={{ background: 'linear-gradient(135deg, #06b6d4, #22d3ee)' }}
title={user?.name || user?.email || 'User'}
>
{initials}
</button>
{userMenuOpen && (
<div
className="absolute right-0 z-50 mt-2 w-56 rounded-lg p-1 shadow-xl animate-scale-in"
style={{ background: 'var(--color-bg-card)', border: '1px solid var(--color-border-default)' }}
>
<div className="px-3 py-2.5 mb-1" style={{ borderBottom: '1px solid var(--color-border-default)' }}>
<p className="text-sm font-medium text-foreground truncate">{user?.name || user?.email}</p>
{effectiveRole && effectiveRole !== 'engineer' && (
<span className="mt-1 inline-flex items-center gap-1 text-xs text-muted-foreground">
<Shield size={10} />
{effectiveRole === 'super_admin' ? 'Super Admin' : effectiveRole === 'owner' ? 'Owner' : 'Viewer'}
</span>
)}
</div>
<Link
to="/account"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-input hover:text-foreground"
>
<Settings size={14} />
Account
</Link>
{isSuperAdmin && (
<Link
to="/admin"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-input hover:text-foreground"
>
<Shield size={14} />
Admin Panel
</Link>
)}
<div className="mt-1 pt-1" style={{ borderTop: '1px solid var(--color-border-default)' }}>
<button
onClick={handleLogout}
className={cn(
'flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm',
'text-muted-foreground hover:bg-input hover:text-foreground'
)}
>
<LogOut size={14} />
Logout
</button>
</div>
</div>
)}
</div>
</div>
</header>
{/* Command Palette */}
<CommandPalette open={commandPaletteOpen} onClose={() => setCommandPaletteOpen(false)} />
<QuickLaunch open={quickLaunchOpen} onClose={() => setQuickLaunchOpen(false)} />
</>
)
}