diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index 97b94a2ed..341a2c698 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -17,9 +17,13 @@ import type { ClientSiteSections, } from './encodeClientSiteSections'; +const DESKTOP_BREAKPOINT = 768; const SCREEN_OFFSET = 16; // 1rem const MAX_ITEMS_PER_COLUMN = 10; // number of items per column -const GROUP_MASONRY_THRESHOLD = 6; // if a section group has more than this many child groups, it will be shown in a masonry grid +const GROUP_MASONRY_THRESHOLD = 3; // if a section group has more than this many child groups, it will be shown in a masonry grid +const COLUMN_WIDTH = '18rem'; +const COLUMN_GAP = '2rem'; +const MAX_MASONRY_COLUMNS = 4; /** * A set of navigational links representing site sections for multi-section sites @@ -37,24 +41,19 @@ export function SiteSectionTabs(props: { const containerRef = React.useRef(null); const currentTriggerRef = React.useRef(null); - const [offset, setOffset] = React.useState(null); const [value, setValue] = React.useState(); - const isMobile = useIsMobile(768); - - React.useEffect(() => { - const trigger = currentTriggerRef.current; - const container = containerRef.current; - if (!value || !trigger || !container) { - return; - } - - const triggerWidth = trigger.getBoundingClientRect().width - SCREEN_OFFSET; - const triggerLeft = - trigger.getBoundingClientRect().left - - (window.innerWidth - container.getBoundingClientRect().width) / 2; - setOffset(triggerLeft + triggerWidth / 2); - }, [value]); + const isMobile = useIsMobile(DESKTOP_BREAKPOINT); + const offset = useNavigationMenuViewportOffset({ + value, + isMobile, + triggerRef: currentTriggerRef, + containerRef, + }); + const viewportLeft = + !isMobile && offset !== null + ? `clamp(0px, calc(${offset - SCREEN_OFFSET}px - var(--radix-navigation-menu-viewport-width, 0px)/2), calc(100% - var(--radix-navigation-menu-viewport-width, 0px)))` + : '0px'; return structure.length > 0 ? ( - - + +
+ +
) : ( @@ -153,23 +169,18 @@ export function SiteSectionTabs(props: {
@@ -177,6 +188,41 @@ export function SiteSectionTabs(props: { ) : null; } +function useNavigationMenuViewportOffset(args: { + value: string | undefined; + isMobile: boolean; + triggerRef: React.RefObject; + containerRef: React.RefObject; +}) { + const { value, isMobile, triggerRef, containerRef } = args; + const [offset, setOffset] = React.useState(null); + + React.useLayoutEffect(() => { + if (isMobile) { + setOffset(null); + return; + } + + if (!value) { + return; + } + + const trigger = triggerRef.current; + const container = containerRef.current; + if (!trigger || !container) { + return; + } + + const containerLeft = container.getBoundingClientRect().left; + const triggerWidth = trigger.getBoundingClientRect().width; + const triggerLeft = trigger.getBoundingClientRect().left - containerLeft; + + setOffset(triggerLeft + triggerWidth / 2); + }, [containerRef, isMobile, triggerRef, value]); + + return offset; +} + /** * A tab representing a section or section group */ @@ -222,14 +268,16 @@ function SectionGroupTileList(props: { const hasSections = sections.length > 0; const hasGroups = groups.length > 0; + const isMasonryLayout = groups.length > GROUP_MASONRY_THRESHOLD; + const masonryColumnCount = Math.min(Math.ceil(groups.length / 2), MAX_MASONRY_COLUMNS); return ( -
+
{/* Non-grouped sections */} {hasSections && (
    GROUP_MASONRY_THRESHOLD - ? 'w-screen md:columns-[15rem]' - : 'flex flex-col justify-start md:flex-row md:items-start', + 'w-full md:w-max md:min-w-0 md:max-w-full', hasSections ? 'border-tint-subtle bg-tint-subtle max-md:border-t md:border-l' : '' )} > - {groups.map((group) => ( - - ))} -
+
    li]:mb-4' + : 'flex w-full flex-col justify-start space-y-8 md:w-max md:flex-row md:items-start md:gap-[var(--site-section-column-gap)] md:space-y-0' + )} + style={ + isMasonryLayout + ? ({ + '--masonry-columns': String(masonryColumnCount), + } as React.CSSProperties) + : undefined + } + > + {groups.map((group) => ( + + ))} +
+
)}
); @@ -278,38 +339,40 @@ function SectionGroupTileList(props: { function SectionGroupTile(props: { child: ClientSiteSection | ClientSiteSectionGroup; currentSection: ClientSiteSection; + invertIcon?: boolean; }) { - const { child, currentSection } = props; + const { child, currentSection, invertIcon } = props; if (child.object === 'site-section') { const { url, icon, title, description } = child; const isActive = child.id === currentSection.id; return ( -
  • +
  • -
    +
    {icon && (
    )} -
    - {title} +
    + {title} {description && (

    {description} @@ -326,15 +389,15 @@ function SectionGroupTile(props: { const { title, icon, children } = child; return ( -

  • -
    +
  • +
    {icon && ( )} - {title} + {title}
      ))}