feat: add command palette search, dashboard rewrite, and shell height fixes (Phase C)

- Add ⌘K command palette with debounced search across flows and sessions
- Rewrite QuickStartPage as dashboard with stats, filters, sessions panel
- Fix h-[calc(100vh-4rem)] → h-full across all pages for CSS Grid shell
- Add active session count badge to sidebar Sessions nav item

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-02-15 01:53:55 -05:00
parent d6f4286570
commit 09cd05e143
9 changed files with 504 additions and 324 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useRef, useEffect } from 'react'
import { useState, useRef, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Search, Zap, Bell, LogOut, User, Shield, Settings } from 'lucide-react'
import { useAuthStore } from '@/store/authStore'
@@ -6,6 +6,7 @@ import { usePermissions } from '@/hooks/usePermissions'
import { useWorkspaceStore } from '@/store/workspaceStore'
import { getWorkspaceLabels } from '@/constants/workspaceLabels'
import { BrandLogo } from '@/components/common/BrandLogo'
import { CommandPalette } from './CommandPalette'
import { cn } from '@/lib/utils'
export function TopBar() {
@@ -16,6 +17,7 @@ export function TopBar() {
const labels = getWorkspaceLabels(activeWorkspace?.slug)
const [userMenuOpen, setUserMenuOpen] = useState(false)
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const handleLogout = async () => {
@@ -34,111 +36,131 @@ export function TopBar() {
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [userMenuOpen])
// ⌘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 flex items-center gap-4 border-b border-border bg-background px-4">
{/* Logo area */}
<Link to="/" className="flex items-center gap-2.5 pr-4" style={{ width: 'calc(260px - 40px)' }}>
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-brand">
<BrandLogo size="sm" className="h-4 w-4" />
</div>
<span className="text-sm font-heading font-bold tracking-tight">
<span className="text-foreground">Resolution</span>
<span className="text-gradient-brand">Flow</span>
</span>
</Link>
<>
<header className="topbar flex items-center gap-4 border-b border-border bg-background px-4">
{/* Logo area */}
<Link to="/" className="flex items-center gap-2.5 pr-4" style={{ width: 'calc(260px - 40px)' }}>
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-brand">
<BrandLogo size="sm" className="h-4 w-4" />
</div>
<span className="text-sm font-heading font-bold tracking-tight">
<span className="text-foreground">Resolution</span>
<span className="text-gradient-brand">Flow</span>
</span>
</Link>
{/* Search bar */}
<div className="relative flex-1" style={{ maxWidth: '480px' }}>
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
<input
type="text"
placeholder={labels.searchPlaceholder}
className="w-full rounded-lg border border-border bg-card py-2 pl-9 pr-14 text-[0.8125rem] text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/20"
/>
<span className="absolute right-3 top-1/2 -translate-y-1/2 rounded border border-border bg-background px-1.5 py-0.5 font-label text-[0.625rem] text-muted-foreground">
K
</span>
</div>
{/* Spacer */}
<div className="flex-1" />
{/* Action buttons */}
<div className="flex items-center gap-1">
<button className="rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors" title="Quick Launch">
<Zap size={18} />
</button>
<button className="relative rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors" title="Notifications">
<Bell size={18} />
{/* Search trigger */}
<button
onClick={() => setCommandPaletteOpen(true)}
className="relative flex-1 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-lg border border-border bg-card py-2 pl-9 pr-14 text-[0.8125rem] text-muted-foreground cursor-pointer hover:border-primary/30 transition-colors">
{labels.searchPlaceholder}
</div>
<span className="absolute right-3 top-1/2 -translate-y-1/2 rounded border border-border bg-background px-1.5 py-0.5 font-label text-[0.625rem] text-muted-foreground">
K
</span>
</button>
{/* 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-full bg-gradient-brand text-xs font-heading font-bold text-white hover:opacity-90 transition-opacity"
title={user?.name || user?.email || 'User'}
>
{initials}
{/* Spacer */}
<div className="flex-1" />
{/* Action buttons */}
<div className="flex items-center gap-1">
<button className="rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors" title="Quick Launch">
<Zap size={18} />
</button>
<button className="relative rounded-lg p-2 text-muted-foreground hover:bg-card hover:text-foreground transition-colors" title="Notifications">
<Bell size={18} />
</button>
{userMenuOpen && (
<div className="absolute right-0 z-50 mt-2 w-56 rounded-lg border border-border bg-card p-1 shadow-xl animate-scale-in">
<div className="border-b border-border px-3 py-2.5 mb-1">
<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-accent hover:text-foreground"
>
<User size={14} />
Account
</Link>
<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-accent hover:text-foreground"
>
<Settings size={14} />
Settings
</Link>
{isSuperAdmin && (
{/* 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-full bg-gradient-brand text-xs font-heading font-bold text-white hover:opacity-90 transition-opacity"
title={user?.name || user?.email || 'User'}
>
{initials}
</button>
{userMenuOpen && (
<div className="absolute right-0 z-50 mt-2 w-56 rounded-lg border border-border bg-card p-1 shadow-xl animate-scale-in">
<div className="border-b border-border px-3 py-2.5 mb-1">
<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="/admin"
to="/account"
onClick={() => setUserMenuOpen(false)}
className="flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
>
<Shield size={14} />
Admin Panel
<User size={14} />
Account
</Link>
)}
<div className="border-t border-border mt-1 pt-1">
<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-accent hover:text-foreground'
)}
<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-accent hover:text-foreground"
>
<LogOut size={14} />
Logout
</button>
<Settings size={14} />
Settings
</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-accent hover:text-foreground"
>
<Shield size={14} />
Admin Panel
</Link>
)}
<div className="border-t border-border mt-1 pt-1">
<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-accent hover:text-foreground'
)}
>
<LogOut size={14} />
Logout
</button>
</div>
</div>
</div>
)}
)}
</div>
</div>
</div>
</header>
</header>
{/* Command Palette */}
<CommandPalette open={commandPaletteOpen} onClose={() => setCommandPaletteOpen(false)} />
</>
)
}