import type { ReactNode } from 'react' import { SearchField } from '@/components/ui/search-field' import { ResponsiveTabs } from '@/components/ui/tab-dropdown' import { cn } from '@/lib/utils' // Tabs are data, not nodes: the shell owns their presentation so every page // gets the same behavior — a centered TextTab row on wide viewports that // collapses into a dropdown when the header can't fit both search and tabs. export interface PageShellTab { id: string label: string /** Count badge. `null` = still loading (renders a skeleton); `undefined` = no badge. */ meta?: string | number | null } interface PageSearchShellProps extends React.ComponentProps<'section'> { children: ReactNode tabs?: PageShellTab[] activeTab?: string onTabChange?: (id: string) => void /** Secondary filters shown full-width on their own row below (expands). */ filters?: ReactNode onSearchChange: (value: string) => void searchPlaceholder: string /** Data-derived rotating placeholder nudges (see SearchField.hints). */ searchHints?: string[] searchValue: string /** Hide the search field when there's nothing to search (empty dataset). */ searchHidden?: boolean /** Right-aligned control in the header's trailing cell (e.g. a refresh button) * so mouse users get a visible affordance for the refresh hotkey. */ searchTrailingAction?: ReactNode } function ShellTabs({ tabs, activeTab, onTabChange }: { tabs: PageShellTab[] activeTab?: string onTabChange?: (id: string) => void }) { return ( onTabChange?.(id)} tabs={tabs} value={activeTab ?? tabs[0]?.id ?? ''} wideClassName="justify-center" /> ) } export function PageSearchShell({ children, className, tabs, activeTab, onTabChange, filters, onSearchChange, searchPlaceholder, searchHints, searchValue, searchHidden = false, searchTrailingAction, ...props }: PageSearchShellProps) { const hasTabs = (tabs?.length ?? 0) > 0 return (
{/* Header lives in the page body, below the window chrome (the shell floats traffic lights over the top titlebar-height strip, which the `pt` clears and leaves draggable). Search left, tabs centered on the page via the 1fr/auto/1fr grid; the trailing 1fr keeps the center honest. */} {/* IMPORTANT: do NOT put `-webkit-app-region: drag` on this header. It spans full width over the band where the floating titlebar icon clusters live, and an overlapping OS drag region eats their clicks at the compositor level (pointer-events / no-drag carve-outs across separate stacking contexts don't reliably fix it on macOS). The shell already supplies a draggable titlebar strip that is `calc()`'d around the icon clusters (see app-shell.tsx), so window dragging still works here. */}
{(hasTabs || !searchHidden) && (
{!searchHidden && ( )}
{hasTabs ? (
) : ( )}
{searchTrailingAction}
)} {filters ?
{filters}
: null}
{children}
) }