a11oy / web /src /components /shell /TopBar.tsx
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame Contribute Delete
3.98 kB
import { useState, useRef, useEffect } from 'react';
import { useOrg } from '../../context/OrgContext';
import type { OrgId } from '../../context/OrgContext';
import { ChevronDown } from 'lucide-react';
export function TopBar() {
const { currentOrg, setOrg } = useOrg();
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
const orgs: { id: OrgId; name: string }[] = [
{ id: 'szl', name: 'a11oy' },
{ id: 'acme', name: 'Acme Industries' },
{ id: 'northwind', name: 'Northwind Labs' }
];
useEffect(() => {
function handleOutsideClick(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setIsOpen(false);
}
}
if (isOpen) {
document.addEventListener('mousedown', handleOutsideClick);
}
return () => document.removeEventListener('mousedown', handleOutsideClick);
}, [isOpen]);
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key === 'Escape') {
setIsOpen(false);
triggerRef.current?.focus();
}
}
function handleOrgSelect(id: OrgId) {
setOrg(id);
setIsOpen(false);
triggerRef.current?.focus();
}
return (
<header className="h-14 border-b border-[var(--color-a11oy-border)] bg-[var(--color-a11oy-deep)] flex items-center justify-between px-6 shrink-0 z-30 sticky top-0">
<div className="flex items-center gap-5">
<div className="flex items-baseline gap-2">
<span className="font-display font-semibold text-lg tracking-tight text-[var(--color-a11oy-text)]">
A<span className="font-mono text-[var(--color-a11oy-blue)] font-bold">11</span>oy
</span>
</div>
<div className="h-4 w-px bg-[var(--color-a11oy-border)]" aria-hidden="true"></div>
<div className="relative" ref={menuRef} onKeyDown={handleKeyDown}>
<button
ref={triggerRef}
type="button"
onClick={() => setIsOpen(prev => !prev)}
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-label={`Current organization: ${orgs.find(o => o.id === currentOrg)?.name}. Change organization`}
className="flex items-center gap-2 text-sm text-[var(--color-a11oy-text-sub)] hover:text-[var(--color-a11oy-text)] transition-colors cursor-pointer"
>
<span className="font-medium">{orgs.find(o => o.id === currentOrg)?.name}</span>
<ChevronDown className="w-3.5 h-3.5 opacity-50" aria-hidden="true" />
</button>
{isOpen && (
<ul
role="listbox"
aria-label="Select organization"
className="absolute top-full left-0 mt-2 w-48 bg-[var(--color-a11oy-card)] border border-[var(--color-a11oy-border)] rounded shadow-xl z-50 py-1"
>
{orgs.map(org => (
<li key={org.id} role="option" aria-selected={currentOrg === org.id}>
<button
type="button"
onClick={() => handleOrgSelect(org.id)}
className={`w-full text-left px-4 py-2 text-sm transition-colors ${currentOrg === org.id ? 'bg-[var(--color-a11oy-surface)] text-[var(--color-a11oy-blue)]' : 'text-[var(--color-a11oy-text-sub)] hover:bg-[var(--color-a11oy-surface)] hover:text-[var(--color-a11oy-text)]'}`}
>
{org.name}
</button>
</li>
))}
</ul>
)}
</div>
</div>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-a11oy-blue)] animate-pulse" aria-hidden="true" />
<span className="text-[11px] font-mono" style={{ color: 'var(--color-a11oy-text-ghost)', letterSpacing: '0.04em' }}>
Governed Environment
</span>
</div>
</header>
);
}