import { useEffect, useState, type ReactNode } from 'react'; import { usePublicConfig } from '../api/hooks'; import { useI18n } from '../i18n'; import { useWorkspace } from '../state/workspace'; import { useSession } from '../state/session'; import { useToasts } from '../state/toasts'; import { goTo, workspacePath } from '../state/route'; import { Menu, useMenu } from './Menu'; import { SearchIcon } from './Icons'; interface TopBarProps { onOpenAccount(): void; onOpenPeople(): void; onOpenKeys(): void; onOpenWorkspace(): void; onOpenBilling(): void; sidebarOpen: boolean; onToggleSidebar(): void; } export function TopBar({ onOpenAccount, onOpenPeople, onOpenKeys, onOpenWorkspace, onOpenBilling, sidebarOpen, onToggleSidebar, }: TopBarProps): ReactNode { const ws = useWorkspace(); const session = useSession(); const toasts = useToasts(); const { t } = useI18n(); // An install under its own licence has no billing to open — the dialog is // still worth reaching for what it counts, so the entry stays and says so. const metered = usePublicConfig().data?.metered ?? true; const accountMenu = useMenu(); const workspaceMenu = useMenu(); // Debounce the search box: every keystroke is a fresh SQL query otherwise. const { setSearch } = ws; const [draft, setDraft] = useState(ws.search); useEffect(() => { const handle = window.setTimeout(() => setSearch(draft.trim()), 250); return () => window.clearTimeout(handle); }, [draft, setSearch]); return (
{/* Only rendered wide enough to matter by CSS: below 980px the sidebar becomes a drawer and this is the only way back to it. */}
Wellformed
{/* The workspace is the first thing on screen, because on a shared account it is the thing you most need to be sure of. A visitor has nothing to switch to, so it is a label rather than a menu. */} {session.visiting ? ( {session.workspace?.name ?? t('topbar.workspace')} {t('topbar.demoReadOnly')} ) : ( )}
setDraft(e.target.value)} placeholder={t('topbar.searchIn', ws.table?.label.toLowerCase() ?? '')} aria-label={t('topbar.searchCurrentTable')} onKeyDown={(e) => { if (e.key === 'Escape') setDraft(''); }} /> {draft && ( )}
{/* Undo is a write, so it is not offered to someone who cannot write. Both halves carry a word and a tooltip naming what they will do; the words fold away on a narrow screen, the glyphs and titles do not. */} {!session.visiting && ( <> )} {session.visiting ? ( {t('topbar.createYourOwn')} ) : ( )} {workspaceMenu.state && ( w.slug !== session.workspace?.slug) .map((w) => ({ label: t('topbar.switchTo', w.name), // Names are not unique; the address is. Showing it is the only // way to tell two workspaces called "Client Work" apart. hint: w.slug, onSelect: () => goTo(workspacePath(w.slug)), })), { label: t('topbar.allWorkspaces'), onSelect: () => goTo('/') }, ]} /> )} {accountMenu.state && ( void session .signOut() .then(() => goTo('/')) .catch((e: unknown) => toasts.fail(e)), }, ]} /> )}
); }