@@ -8,6 +8,10 @@ import { useLocalStorage } from '~/utils/useLocalStorage'
88import { useClickOutside } from '~/hooks/useClickOutside'
99import { last } from '~/utils/utils'
1010import type { ConfigSchema , MenuItem } from '~/utils/config'
11+ import {
12+ getActiveDocsNavTabId ,
13+ getTabbedMenuConfig ,
14+ } from '~/utils/docsNavTabs'
1115import { Framework , LibraryId } from '~/libraries'
1216import { frameworkOptions } from '~/libraries/frameworks'
1317import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG'
@@ -520,6 +524,7 @@ const useMenuConfig = ({
520524
521525 return {
522526 label : section . label ,
527+ tab : section . tab ,
523528 children,
524529 collapsible : section . collapsible ?? false ,
525530 defaultCollapsed : section . defaultCollapsed ?? false ,
@@ -569,22 +574,42 @@ export function DocsLayout({
569574
570575 const detailsRef = React . useRef < HTMLElement > ( null ! )
571576
577+ const docsMatch = matches . find ( ( d ) => d . pathname . includes ( '/docs' ) )
578+ const docsPathname = docsMatch ?. pathname ?? ''
579+
580+ const relativePathname = lastMatch . pathname . replace ( docsPathname + '/' , '' )
581+
582+ const tabbedMenuConfig = React . useMemo ( ( ) => {
583+ return getTabbedMenuConfig ( menuConfig )
584+ } , [ menuConfig ] )
585+
586+ const activeTabId = React . useMemo ( ( ) => {
587+ return getActiveDocsNavTabId ( {
588+ isExample,
589+ menuConfig,
590+ pathname : lastMatch . pathname ,
591+ relativePathname,
592+ } )
593+ } , [ isExample , lastMatch . pathname , menuConfig , relativePathname ] )
594+
595+ const visibleMenuConfig = React . useMemo ( ( ) => {
596+ return (
597+ tabbedMenuConfig . find ( ( tab ) => tab . id === activeTabId ) ?. groups ??
598+ menuConfig
599+ )
600+ } , [ activeTabId , menuConfig , tabbedMenuConfig ] )
601+
572602 const flatMenu = React . useMemo (
573- ( ) => menuConfig . flatMap ( ( d ) => d ? .children ) ,
574- [ menuConfig ] ,
603+ ( ) => visibleMenuConfig . flatMap ( ( d ) => d . children ) ,
604+ [ visibleMenuConfig ] ,
575605 )
576606
577607 // Filter out external links for prev/next navigation
578608 const internalFlatMenu = React . useMemo (
579- ( ) => flatMenu . filter ( ( d ) => d && ! d . to . startsWith ( 'http' ) ) ,
609+ ( ) => flatMenu . filter ( ( d ) => ! d . to . startsWith ( 'http' ) ) ,
580610 [ flatMenu ] ,
581611 )
582612
583- const docsMatch = matches . find ( ( d ) => d . pathname . includes ( '/docs' ) )
584- const docsPathname = docsMatch ?. pathname ?? ''
585-
586- const relativePathname = lastMatch . pathname . replace ( docsPathname + '/' , '' )
587-
588613 const index = internalFlatMenu . findIndex ( ( d ) => d ?. to === relativePathname )
589614 const prevItem = internalFlatMenu [ index - 1 ]
590615 const nextItem = internalFlatMenu [ index + 1 ]
@@ -600,19 +625,22 @@ export function DocsLayout({
600625 const activePartners = partners . filter ( ( d ) => d . status === 'active' )
601626
602627 const groupInitialOpenState = React . useMemo ( ( ) => {
603- return menuConfig . reduce < Record < string , boolean > > ( ( acc , group , index ) => {
604- const isChildActive = group . children . some ( ( child ) => child . to === _splat )
605- const key = `${ index } :${ String ( group . label ) } `
606-
607- acc [ key ] = isChildActive
608- ? true
609- : typeof group . defaultCollapsed !== 'undefined'
610- ? ! group . defaultCollapsed
611- : false
612-
613- return acc
614- } , { } )
615- } , [ menuConfig , _splat ] )
628+ return visibleMenuConfig . reduce < Record < string , boolean > > (
629+ ( acc , group , index ) => {
630+ const isChildActive = group . children . some ( ( child ) => child . to === _splat )
631+ const key = `${ index } :${ String ( group . label ) } `
632+
633+ acc [ key ] = isChildActive
634+ ? true
635+ : typeof group . defaultCollapsed !== 'undefined'
636+ ? ! group . defaultCollapsed
637+ : false
638+
639+ return acc
640+ } ,
641+ { } ,
642+ )
643+ } , [ visibleMenuConfig , _splat ] )
616644
617645 const [ openGroups , setOpenGroups ] = React . useState ( groupInitialOpenState )
618646
@@ -638,7 +666,7 @@ export function DocsLayout({
638666 } )
639667 } , [ groupInitialOpenState ] )
640668
641- const menuItems = menuConfig . map ( ( group , i ) => {
669+ const menuItems = visibleMenuConfig . map ( ( group , i ) => {
642670 const groupKey = `${ i } :${ String ( group . label ) } `
643671
644672 const groupContent = (
@@ -808,7 +836,7 @@ export function DocsLayout({
808836 ) }
809837 >
810838 < DocsMenuStrip
811- menuConfig = { menuConfig }
839+ menuConfig = { visibleMenuConfig }
812840 activeItem = { relativePathname }
813841 fullPathname = { lastMatch . pathname }
814842 colorFrom = { colorFrom }
@@ -835,7 +863,7 @@ export function DocsLayout({
835863 < div
836864 ref = { expandedMenuRef }
837865 className = { twMerge (
838- 'max- w-[250px] xl:max- w-[300px] 2xl:max- w-[400px]' ,
866+ 'w-[250px] xl:w-[300px] 2xl:w-[400px] shrink-0 ' ,
839867 'flex-col overflow-hidden' ,
840868 'h-[calc(100dvh-var(--navbar-height))] top-[var(--navbar-height)]' ,
841869 'z-20 border-r border-gray-500/20' ,
@@ -882,6 +910,47 @@ export function DocsLayout({
882910 </ >
883911 )
884912
913+ const docsTabs = (
914+ < div className = "border-b border-gray-500/20 bg-white/70 dark:bg-black/40 backdrop-blur-lg" >
915+ < nav
916+ aria-label = "Documentation sections"
917+ className = "flex items-center gap-1 overflow-x-auto px-3 md:px-6 py-2 text-sm"
918+ >
919+ { tabbedMenuConfig . map ( ( tab ) => {
920+ const target = tab . firstItem
921+ const isActive = tab . id === activeTabId
922+
923+ if ( ! target ) {
924+ return null
925+ }
926+
927+ const linkParams =
928+ ! target . to . startsWith ( '/' ) || target . to . includes ( '/$libraryId' )
929+ ? ( { libraryId, version } as never )
930+ : undefined
931+
932+ return (
933+ < Link
934+ key = { tab . id }
935+ from = "/$libraryId/$version/docs"
936+ to = { target . to }
937+ params = { linkParams }
938+ className = { twMerge (
939+ 'whitespace-nowrap rounded-md px-3 py-1.5 font-semibold transition-colors' ,
940+ 'hover:bg-gray-500/10' ,
941+ isActive
942+ ? `bg-gray-500/10 text-transparent bg-clip-text bg-linear-to-r ${ colorFrom } ${ colorTo } `
943+ : 'opacity-70 hover:opacity-100' ,
944+ ) }
945+ >
946+ { tab . label }
947+ </ Link >
948+ )
949+ } ) }
950+ </ nav >
951+ </ div >
952+ )
953+
885954 return (
886955 < WidthToggleContext . Provider value = { { isFullWidth, setIsFullWidth } } >
887956 < DocNavigationContext . Provider
@@ -907,13 +976,15 @@ export function DocsLayout({
907976 < div
908977 className = { twMerge (
909978 'flex flex-col max-w-full min-w-0 flex-1 min-h-0 relative' ,
910- ! isLandingPage && 'px-4 md:px-8' ,
911979 ) }
912980 >
981+ { docsTabs }
913982 < div
914983 className = { twMerge (
915984 `max-w-full min-w-0 flex flex-col justify-center w-full` ,
916985
986+ ! isLandingPage && 'px-4 md:px-8' ,
987+
917988 ! isLandingPage &&
918989 ! isExample &&
919990 ! isNpmStats &&
0 commit comments