import { useEffect, useRef, type ReactNode } from 'react' import { Button as BaseButton } from '@base-ui/react/button' import { cn } from './cn' import { Kbd } from './Kbd' import { FIELD_SHELL_CLASSES } from './looks' import { shortcutLabel } from './shortcuts' /** * The 52px top bar, in three manners: * * - `solid` — in flow: a hairline under it, the surface behind it, the * frame's first row (2026-09-02, verbatim from the structured app; its * product title moved to the caller). * - `floating` — an overlay: absolute over the content, no border and no * background, and the bar itself ignores the pointer — only its clusters * catch clicks, so the content underneath stays reachable (2026-09-02, * verbatim from the floating app). * - `inset` — in flow like `solid`, but standing on the ground: no hairline * and no surface, above a sidebar on the ground and the content's card * (Katerina, 28 September). * * Geometry both apps agreed on before extraction: 52px tall, `pl-5 * pr-[26px]`, right cluster `gap-[6px]`. The left region is two slots * side by side — `menu` (the menu button, in an app whose navigation * collapses) and `logo` (the mark or name beside it; Katerina, * 2026-09-02). This component does not know who you are or what is being * searched; it only knows where those go. * * **Search** (Katerina, 8 October: "topbar has a variant with a search input, * I assumed it would use it"). Given `onSearch`, the bar draws the search * field itself — `SearchInput`'s look, 290px, as a button that opens what the * app searches with — and Ctrl+K (Cmd+K on a Mac) presses it from anywhere. * Before, every app drew its own field and listened for the keys itself. * A `search` of the caller's own still takes the centre. * * **A click leaves focus where it was**, as Peek's field did: what searching * opens often acts on what you were in — Peek's launcher reads the composer * you were typing in and its selection — and taking focus would leave it * nothing. Tab still reaches the field, and Enter or Space presses it. */ export interface TopBarProps { variant?: 'solid' | 'floating' | 'inset' /** Far left: the menu button, in an app whose navigation collapses. Stays the caller's — the bar only places it. */ menu?: ReactNode /** Beside the menu button: the app's logo mark, or its name as text. */ logo?: ReactNode /** The centre: something of the caller's own. Without it, and with `onSearch`, the bar draws its search field. */ search?: ReactNode /** What searching opens — the app's command launcher. Called on a click on the search field, and on Ctrl+K (Cmd+K) anywhere. */ onSearch?: () => void /** The words in the bar's own search field. Default "Search…". */ searchPlaceholder?: string /** The right cluster — an `IdentityMenu` and its neighbours. */ right?: ReactNode className?: string } export function TopBar({ variant = 'solid', menu, logo, search, onSearch, searchPlaceholder = 'Search…', right, className }: TopBarProps) { const floating = variant === 'floating' // The latest handler, so the key listener is set once rather than on every render. const searchRef = useRef(onSearch) useEffect(() => { searchRef.current = onSearch }) const searches = onSearch !== undefined useEffect(() => { if (!searches) return const onKey = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { event.preventDefault() searchRef.current?.() } } // @estiva-escape(no-hand-rolled-behaviour): Ctrl+K opens search from anywhere on the page, which is a page-wide shortcut and not a floating part; no part listens for a key across the page window.addEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey) }, [searches]) const centre = search ?? (onSearch && ( event.preventDefault()} onClick={onSearch} className={cn('flex w-[290px] cursor-pointer items-center gap-2 px-3 py-2', FIELD_SHELL_CLASSES)}> {searchPlaceholder} {shortcutLabel('Mod-K')} )) return (
{(menu || logo) && (
{menu} {logo &&
{logo}
}
)}
{centre}
{right}
) }