diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 9335ddc2..2729a847 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -8,6 +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 { Framework, LibraryId } from '~/libraries' import { frameworkOptions } from '~/libraries/frameworks' import { DocsCalloutQueryGG } from '~/components/DocsCalloutQueryGG' @@ -520,6 +521,7 @@ const useMenuConfig = ({ return { label: section.label, + tab: section.tab, children, collapsible: section.collapsible ?? false, defaultCollapsed: section.defaultCollapsed ?? false, @@ -569,22 +571,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 +622,24 @@ 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 +665,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 +835,7 @@ export function DocsLayout({ )} > -
+
@@ -882,6 +909,55 @@ export function DocsLayout({ ) + const docsTabs = ( +
+ +
+ ) + return ( + {docsTabs}
{!isLandingPage && ( - + = [ + { 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' +}