PageHeaderProvider.tsx
1 import { useLayoutEffect, useMemo, useState, type ReactNode } from "react"; 2 import { useLocation } from "react-router-dom"; 3 import { PageHeaderContext } from "./page-header-context"; 4 import { resolvePageTitle } from "@/lib/resolve-page-title"; 5 import { cn } from "@/lib/utils"; 6 import { useI18n } from "@/i18n"; 7 8 export function PageHeaderProvider({ 9 children, 10 pluginTabs, 11 }: { 12 children: ReactNode; 13 pluginTabs: { path: string; label: string }[]; 14 }) { 15 const { pathname } = useLocation(); 16 const { t } = useI18n(); 17 const [titleOverride, setTitleOverride] = useState<string | null>(null); 18 const [afterTitle, setAfterTitle] = useState<ReactNode>(null); 19 const [end, setEnd] = useState<ReactNode>(null); 20 21 // Clear any per-page title / toolbar slots when the path changes. Child routes 22 // re-fill these on mount via usePageHeader. 23 /* eslint-disable react-hooks/set-state-in-effect */ 24 useLayoutEffect(() => { 25 setTitleOverride(null); 26 setAfterTitle(null); 27 setEnd(null); 28 }, [pathname]); 29 /* eslint-enable react-hooks/set-state-in-effect */ 30 31 const defaultTitle = useMemo( 32 () => resolvePageTitle(pathname, t, pluginTabs), 33 [pathname, t, pluginTabs], 34 ); 35 const displayTitle = titleOverride ?? defaultTitle; 36 37 const isChatRoute = pathname === "/chat" || pathname === "/chat/"; 38 39 const value = useMemo( 40 () => ({ 41 setAfterTitle, 42 setEnd, 43 setTitle: setTitleOverride, 44 }), 45 [], 46 ); 47 48 return ( 49 <PageHeaderContext.Provider value={value}> 50 <div className="flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-hidden"> 51 <header 52 className={cn( 53 "z-1 w-full shrink-0", 54 "box-border h-14 min-h-14", 55 "border-b border-current/20", 56 "bg-background-base/40 backdrop-blur-sm", 57 "overflow-hidden", 58 "sm:min-h-0", 59 )} 60 role="banner" 61 > 62 <div 63 className={cn( 64 "flex h-full w-full min-w-0 flex-1 gap-2 px-3 py-2 sm:gap-3 sm:px-6 sm:py-0", 65 isChatRoute 66 ? "flex-row items-center" 67 : "flex-col justify-center sm:flex-row sm:items-center", 68 )} 69 > 70 <div className="flex min-w-0 flex-1 items-center gap-2 sm:gap-3"> 71 <h1 72 className="font-expanded min-w-0 truncate text-sm font-bold tracking-[0.08em] text-midground" 73 style={{ mixBlendMode: "plus-lighter" }} 74 > 75 {displayTitle} 76 </h1> 77 {afterTitle} 78 </div> 79 80 {end ? ( 81 <div 82 className={cn( 83 "flex min-w-0 justify-end sm:max-w-md sm:flex-1", 84 isChatRoute ? "w-auto shrink-0" : "w-full", 85 )} 86 > 87 {end} 88 </div> 89 ) : null} 90 </div> 91 </header> 92 93 <main 94 className={cn( 95 "min-h-0 w-full min-w-0 flex-1 flex flex-col", 96 isChatRoute 97 ? "overflow-hidden" 98 : "overflow-y-auto overflow-x-hidden [scrollbar-gutter:stable]", 99 )} 100 > 101 {children} 102 </main> 103 </div> 104 </PageHeaderContext.Provider> 105 ); 106 }