diff --git a/src/components/Collapsible.tsx b/src/components/Collapsible.tsx index f7a3941ca..c187615dd 100644 --- a/src/components/Collapsible.tsx +++ b/src/components/Collapsible.tsx @@ -16,9 +16,8 @@ type CollapsibleProps = { className?: string } -type CollapsibleTriggerProps = { +type CollapsibleTriggerProps = React.ButtonHTMLAttributes & { children: React.ReactNode - className?: string } type CollapsibleContentProps = { @@ -76,16 +75,26 @@ export function Collapsible({ export function CollapsibleTrigger({ children, className, + onClick, + onMouseDown, + type = 'button', + ...props }: CollapsibleTriggerProps) { - const { toggle } = useCollapsible() + const { open, toggle } = useCollapsible() return ( - -
- - - -
-
TanStack
- - + typeof library.to === 'string' && + library.to.startsWith('/') && + library.visible !== false ) } -const MobileCard = ({ - children, - isActive, -}: { - children: React.ReactNode - isActive?: boolean -}) => ( - - {children} - -) +function getLibraryDisplayName(library: LibrarySlim) { + return library.name.replace(/^TanStack\s+/, '') +} + +function isExternalLink(to: string) { + return to.startsWith('http') || to.startsWith('mailto:') +} + +function getLibraryDocsTo(library: NavigationLibrary) { + return `${library.to}/latest/docs` +} + +function getLibraryMenuGroups() { + return LIBRARY_MENU_GROUP_IDS.map((groupId) => { + const groupLibraries = librariesByGroup[groupId] + const libraries = groupLibraries.filter(isNavigationLibrary) + + return { + id: groupId, + label: librariesGroupNamesMap[groupId], + libraries, + } + }).filter((group) => group.libraries.length > 0) +} function AiDockMount() { const { isAiDockOpen } = useSearchContext() @@ -164,6 +413,7 @@ function AiDockMount() { export function Navbar({ children }: { children: React.ReactNode }) { const matches = useMatches() + const location = useLocation() const { Title } = React.useMemo(() => { const match = [...matches].reverse().find((m) => m.staticData.Title) @@ -186,7 +436,7 @@ export function Navbar({ children }: { children: React.ReactNode }) { } } - updateContainerHeight() // Initial call to set the height + updateContainerHeight() window.addEventListener('resize', updateContainerHeight) return () => { @@ -194,10 +444,12 @@ export function Navbar({ children }: { children: React.ReactNode }) { } }, []) - const [showMenu, setShowMenu] = React.useState(false) + const [mobileMenuOpen, setMobileMenuOpen] = React.useState(false) const [canLoadAuthControls, setCanLoadAuthControls] = React.useState(false) - const largeMenuRef = React.useRef(null) - const menuButtonRef = React.useRef(null) + + React.useEffect(() => { + setMobileMenuOpen(false) + }, [location.pathname, location.hash]) React.useEffect(() => { if (typeof window === 'undefined') { @@ -226,59 +478,68 @@ export function Navbar({ children }: { children: React.ReactNode }) { } }, []) - // Close mobile menu when clicking outside - const smallMenuRef = useClickOutside({ - enabled: showMenu, - onClickOutside: () => setShowMenu(false), - additionalRefs: [largeMenuRef, menuButtonRef], - }) + React.useEffect(() => { + if (!mobileMenuOpen) { + return + } + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + setMobileMenuOpen(false) + } + } + + document.addEventListener('keydown', onKeyDown) - const loginButtonFallback = ( + return () => { + document.removeEventListener('keydown', onKeyDown) + } + }, [mobileMenuOpen]) + + const getLoginButtonFallback = (className?: string) => ( Log In ) + const renderAuthControls = (className?: string) => + canLoadAuthControls ? ( + + + + ) : ( + getLoginButtonFallback(className) + ) const socialLinks = + const siteBackdropActive = mobileMenuOpen const navbar = (
-
+
- - } - > + }> - + {Title ? ( @@ -287,495 +548,570 @@ export function Navbar({ children }: { children: React.ReactNode }) {
) : null}
+ +
-
-
{socialLinks}
+ +
+
{socialLinks}
-
- -
+ -
- {canLoadAuthControls ? ( - - - +
+ {renderAuthControls()} +
+
+
) - const activeLibrary = useLocation({ - select: (location) => { - return libraries.find((library) => { - return library.to && location.pathname.startsWith(library.to) - }) - }, - }) - - const linkClasses = `flex items-center justify-between gap-2 group px-3 py-3 md:px-2 md:py-1 rounded-lg hover:bg-gray-500/10 font-bold text-base md:text-sm` - - const items = ( -
-
- {(() => { - return libraries - .filter( - ( - d, - ): d is LibrarySlim & { - to: string - textStyle: string - badge?: string - colorFrom: string - } => - d.to !== undefined && - d.visible !== false && - (SIDEBAR_LIBRARY_IDS as readonly string[]).includes(d.id), - ) - .sort((a, b) => { - const indexA = SIDEBAR_LIBRARY_IDS.indexOf( - a.id as (typeof SIDEBAR_LIBRARY_IDS)[number], - ) - const indexB = SIDEBAR_LIBRARY_IDS.indexOf( - b.id as (typeof SIDEBAR_LIBRARY_IDS)[number], - ) - return indexA - indexB - }) - })().map((library, i) => { - const [_, name] = library.name.split(' ') - const isActive = library.to === activeLibrary?.to - - return ( -
- {library.to?.startsWith('http') ? ( - <> - {/* Mobile: Card wrapper */} - - - - {name} - - - {/* Desktop: no card */} - - - {name} - - - ) : ( - <> - {/* Mobile: Direct link with Card */} - - - - - {name} - - {library.badge ? ( - - {library.badge} - - ) : null} - - - {/* Desktop: Simple link */} - - - - {name} - - {library.badge ? ( - - {library.badge} - - ) : null} - - - )} -
- ) - })} - {/* Mobile: More Libraries card */} - - -
- -
More Libraries
-
- -
- {/* Desktop: More Libraries link */} - + +
-
- -
More Libraries
+
+
+ {socialLinks} + {renderAuthControls('h-9 px-3 text-sm')} +
+
- -
-
+
+ + + ) + + return ( + <> + {navbar} + {mobileMenu} +