Skip to content

Commit a37bd2d

Browse files
committed
Refactor docs nav into tabbed sections
1 parent b247d8e commit a37bd2d

3 files changed

Lines changed: 270 additions & 25 deletions

File tree

src/components/DocsLayout.tsx

Lines changed: 96 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { useLocalStorage } from '~/utils/useLocalStorage'
88
import { useClickOutside } from '~/hooks/useClickOutside'
99
import { last } from '~/utils/utils'
1010
import type { ConfigSchema, MenuItem } from '~/utils/config'
11+
import {
12+
getActiveDocsNavTabId,
13+
getTabbedMenuConfig,
14+
} from '~/utils/docsNavTabs'
1115
import { Framework, LibraryId } from '~/libraries'
1216
import { frameworkOptions } from '~/libraries/frameworks'
1317
import { 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 &&

src/utils/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,36 @@ import {
33
fetchRepoFile,
44
isRecoverableGitHubContentError,
55
} from './documents.server'
6+
import { docsNavTabIds, type DocsNavTabId } from './docsNavTabs'
67
import { createServerFn } from '@tanstack/react-start'
78
import { setResponseHeaders } from '@tanstack/react-start/server'
89

910
export type MenuItem = {
1011
label: string | React.ReactNode
12+
tab?: DocsNavTabId
1113
children: {
1214
label: string | React.ReactNode
1315
to: string
1416
badge?: string
17+
tab?: DocsNavTabId
1518
}[]
1619
collapsible?: boolean
1720
defaultCollapsed?: boolean
1821
}
1922

23+
const tabSchema = v.optional(v.picklist(docsNavTabIds))
24+
2025
const configSchema = v.object({
2126
sections: v.array(
2227
v.object({
2328
label: v.string(),
29+
tab: tabSchema,
2430
children: v.array(
2531
v.object({
2632
label: v.string(),
2733
to: v.string(),
2834
badge: v.optional(v.string()),
35+
tab: tabSchema,
2936
}),
3037
),
3138
frameworks: v.optional(
@@ -37,6 +44,7 @@ const configSchema = v.object({
3744
label: v.string(),
3845
to: v.string(),
3946
badge: v.optional(v.string()),
47+
tab: tabSchema,
4048
}),
4149
),
4250
}),

0 commit comments

Comments
 (0)