From a37bd2d51fbcccf952344809aa8e7e3d764a9daf Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Fri, 8 May 2026 12:15:44 -0700 Subject: [PATCH 1/3] Refactor docs nav into tabbed sections --- src/components/DocsLayout.tsx | 121 ++++++++++++++++++++----- src/utils/config.ts | 8 ++ src/utils/docsNavTabs.ts | 166 ++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 src/utils/docsNavTabs.ts diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 9335ddc29..21e535876 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -8,6 +8,10 @@ import { useLocalStorage } from '~/utils/useLocalStorage' import { useClickOutside } from '~/hooks/useClickOutside' import { last } from '~/utils/utils' import type { ConfigSchema, MenuItem } from '~/utils/config' +import { + getActiveDocsNavTabId, + getTabbedMenuConfig, +} from '~/utils/docsNavTabs' import { Framework, LibraryId } from '~/libraries' import { frameworkOptions } from '~/libraries/frameworks' import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG' @@ -520,6 +524,7 @@ const useMenuConfig = ({ return { label: section.label, + tab: section.tab, children, collapsible: section.collapsible ?? false, defaultCollapsed: section.defaultCollapsed ?? false, @@ -569,22 +574,42 @@ export function DocsLayout({ const detailsRef = React.useRef(null!) + const docsMatch = matches.find((d) => d.pathname.includes('/docs')) + const docsPathname = docsMatch?.pathname ?? '' + + const relativePathname = lastMatch.pathname.replace(docsPathname + '/', '') + + const tabbedMenuConfig = React.useMemo(() => { + return getTabbedMenuConfig(menuConfig) + }, [menuConfig]) + + const activeTabId = React.useMemo(() => { + return getActiveDocsNavTabId({ + isExample, + menuConfig, + pathname: lastMatch.pathname, + relativePathname, + }) + }, [isExample, lastMatch.pathname, menuConfig, relativePathname]) + + const visibleMenuConfig = React.useMemo(() => { + return ( + tabbedMenuConfig.find((tab) => tab.id === activeTabId)?.groups ?? + menuConfig + ) + }, [activeTabId, menuConfig, tabbedMenuConfig]) + const flatMenu = React.useMemo( - () => menuConfig.flatMap((d) => d?.children), - [menuConfig], + () => visibleMenuConfig.flatMap((d) => d.children), + [visibleMenuConfig], ) // Filter out external links for prev/next navigation const internalFlatMenu = React.useMemo( - () => flatMenu.filter((d) => d && !d.to.startsWith('http')), + () => flatMenu.filter((d) => !d.to.startsWith('http')), [flatMenu], ) - const docsMatch = matches.find((d) => d.pathname.includes('/docs')) - const docsPathname = docsMatch?.pathname ?? '' - - const relativePathname = lastMatch.pathname.replace(docsPathname + '/', '') - const index = internalFlatMenu.findIndex((d) => d?.to === relativePathname) const prevItem = internalFlatMenu[index - 1] const nextItem = internalFlatMenu[index + 1] @@ -600,19 +625,22 @@ export function DocsLayout({ const activePartners = partners.filter((d) => d.status === 'active') const groupInitialOpenState = React.useMemo(() => { - return menuConfig.reduce>((acc, group, index) => { - const isChildActive = group.children.some((child) => child.to === _splat) - const key = `${index}:${String(group.label)}` - - acc[key] = isChildActive - ? true - : typeof group.defaultCollapsed !== 'undefined' - ? !group.defaultCollapsed - : false - - return acc - }, {}) - }, [menuConfig, _splat]) + return visibleMenuConfig.reduce>( + (acc, group, index) => { + const isChildActive = group.children.some((child) => child.to === _splat) + const key = `${index}:${String(group.label)}` + + acc[key] = isChildActive + ? true + : typeof group.defaultCollapsed !== 'undefined' + ? !group.defaultCollapsed + : false + + return acc + }, + {}, + ) + }, [visibleMenuConfig, _splat]) const [openGroups, setOpenGroups] = React.useState(groupInitialOpenState) @@ -638,7 +666,7 @@ export function DocsLayout({ }) }, [groupInitialOpenState]) - const menuItems = menuConfig.map((group, i) => { + const menuItems = visibleMenuConfig.map((group, i) => { const groupKey = `${i}:${String(group.label)}` const groupContent = ( @@ -808,7 +836,7 @@ export function DocsLayout({ )} > ) + const docsTabs = ( +
+ +
+ ) + return ( + {docsTabs}
= [ + { id: 'get-started', label: 'Get Started' }, + { id: 'tutorial', label: 'Tutorial' }, + { id: 'guides', label: 'Guides' }, + { id: 'api', label: 'API' }, + { id: 'examples', label: 'Examples' }, + { id: 'community', label: 'Community' }, +] + +function getLabelText(label: ReactNode) { + return typeof label === 'string' ? label : '' +} + +function getFallbackDocsNavTabId( + group: MenuItem, + child: MenuItem['children'][number], +): DocsNavTabId { + const groupLabel = getLabelText(group.label).toLowerCase() + const childLabel = getLabelText(child.label).toLowerCase() + const to = child.to.toLowerCase() + const searchText = `${groupLabel} ${childLabel} ${to}` + + if ( + child.to.startsWith('http') || + searchText.includes('community') || + searchText.includes('contributors') || + searchText.includes('npm-stats') || + searchText.includes('npm stats') || + searchText.includes('github') || + searchText.includes('discord') || + searchText.includes('youtube') + ) { + return 'community' + } + + if (searchText.includes('example')) { + return 'examples' + } + + if ( + to.includes('/api/') || + to.startsWith('api/') || + to.includes('/reference/') || + to.startsWith('reference/') || + groupLabel.includes('api') || + groupLabel.includes('reference') + ) { + return 'api' + } + + if (searchText.includes('tutorial')) { + return 'tutorial' + } + + if ( + groupLabel.includes('get started') || + groupLabel.includes('getting started') || + groupLabel.includes('overview') || + childLabel === 'home' || + childLabel === 'frameworks' || + searchText.includes('installation') || + searchText.includes('quick-start') || + searchText.includes('quick start') || + searchText.includes('introduction') || + searchText.includes('overview') + ) { + return 'get-started' + } + + return 'guides' +} + +export function getDocsNavTabId( + group: MenuItem, + child: MenuItem['children'][number], +) { + return child.tab ?? group.tab ?? getFallbackDocsNavTabId(group, child) +} + +export function getTabbedMenuConfig(menuConfig: MenuItem[]) { + return docsNavTabs + .map((tab) => { + const groups = menuConfig + .map((group) => { + const children = group.children.filter((child) => { + return getDocsNavTabId(group, child) === tab.id + }) + + return children.length + ? { + ...group, + children, + } + : undefined + }) + .filter((group): group is MenuItem => group !== undefined) + + return { + ...tab, + groups, + firstItem: groups + .flatMap((group) => group.children) + .find((child) => !child.to.startsWith('http')), + } + }) + .filter((tab) => tab.groups.length) +} + +export function getActiveDocsNavTabId({ + isExample, + menuConfig, + pathname, + relativePathname, +}: { + isExample: boolean + menuConfig: MenuItem[] + pathname: string + relativePathname: string +}) { + if (isExample) { + return 'examples' + } + + const activeGroup = menuConfig.find((group) => + group.children.some((child) => child.to === relativePathname), + ) + const activeChild = activeGroup?.children.find( + (child) => child.to === relativePathname, + ) + + if (activeGroup && activeChild) { + return getDocsNavTabId(activeGroup, activeChild) + } + + if (pathname.includes('/docs/api/') || pathname.includes('/docs/reference/')) { + return 'api' + } + + if (pathname.includes('/docs/tutorial')) { + return 'tutorial' + } + + if ( + pathname.includes('/docs/community') || + pathname.includes('/docs/contributors') || + pathname.includes('/docs/npm-stats') + ) { + return 'community' + } + + return 'get-started' +} From 773626a979d7b39630406e0c04fbd01ba1b4736e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 19:55:30 +0000 Subject: [PATCH 2/3] ci: apply automated fixes --- src/components/DocsLayout.tsx | 9 ++++----- src/utils/docsNavTabs.ts | 5 ++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 21e535876..0ef86b9e2 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -8,10 +8,7 @@ import { useLocalStorage } from '~/utils/useLocalStorage' import { useClickOutside } from '~/hooks/useClickOutside' import { last } from '~/utils/utils' import type { ConfigSchema, MenuItem } from '~/utils/config' -import { - getActiveDocsNavTabId, - getTabbedMenuConfig, -} from '~/utils/docsNavTabs' +import { getActiveDocsNavTabId, getTabbedMenuConfig } from '~/utils/docsNavTabs' import { Framework, LibraryId } from '~/libraries' import { frameworkOptions } from '~/libraries/frameworks' import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG' @@ -627,7 +624,9 @@ export function DocsLayout({ const groupInitialOpenState = React.useMemo(() => { return visibleMenuConfig.reduce>( (acc, group, index) => { - const isChildActive = group.children.some((child) => child.to === _splat) + const isChildActive = group.children.some( + (child) => child.to === _splat, + ) const key = `${index}:${String(group.label)}` acc[key] = isChildActive diff --git a/src/utils/docsNavTabs.ts b/src/utils/docsNavTabs.ts index 6f19eb12d..faffe002c 100644 --- a/src/utils/docsNavTabs.ts +++ b/src/utils/docsNavTabs.ts @@ -146,7 +146,10 @@ export function getActiveDocsNavTabId({ return getDocsNavTabId(activeGroup, activeChild) } - if (pathname.includes('/docs/api/') || pathname.includes('/docs/reference/')) { + if ( + pathname.includes('/docs/api/') || + pathname.includes('/docs/reference/') + ) { return 'api' } From 48fbfce87707aee305328a71d226a62fd765176a Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Fri, 8 May 2026 13:20:59 -0700 Subject: [PATCH 3/3] fix gap and add floating --- src/components/DocsLayout.tsx | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 0ef86b9e2..2729a8478 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -862,7 +862,7 @@ export function DocsLayout({
-
+
@@ -910,10 +910,10 @@ export function DocsLayout({ ) const docsTabs = ( -
+
{!isLandingPage && ( - +