From 66ce3e3c62bda281cdc19beae82520094bb802ba Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Mon, 15 Sep 2025 20:06:28 +0200 Subject: [PATCH 1/4] Refactor site section tabs --- .changeset/spicy-boats-change.md | 5 + .../gitbook/src/components/Header/Header.tsx | 46 +-- .../src/components/RootLayout/globals.css | 1 + .../SiteSections/SiteSectionTabs.tsx | 291 ++++++++---------- .../src/components/primitives/Button.tsx | 8 +- 5 files changed, 155 insertions(+), 196 deletions(-) create mode 100644 .changeset/spicy-boats-change.md diff --git a/.changeset/spicy-boats-change.md b/.changeset/spicy-boats-change.md new file mode 100644 index 0000000000..42cb7ea6e3 --- /dev/null +++ b/.changeset/spicy-boats-change.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Refactor section tabs diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index e3be0abba8..fbcba97090 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -163,42 +163,18 @@ export function Header(props: { {sections ? ( -
-
-
- {sections.list.some((s) => s.object === 'site-section-group') || // If there's even a single group, show the tabs - sections.list.length > 1 ? ( // Otherwise, show the tabs if there's more than one section - + + {withVariants === 'translations' ? ( +
+ - ) : null} - {withVariants === 'translations' ? ( -
- -
- ) : null} -
-
+
+ ) : null} +
) : null} diff --git a/packages/gitbook/src/components/RootLayout/globals.css b/packages/gitbook/src/components/RootLayout/globals.css index 9c86654aab..b37f224e9b 100644 --- a/packages/gitbook/src/components/RootLayout/globals.css +++ b/packages/gitbook/src/components/RootLayout/globals.css @@ -217,6 +217,7 @@ ::-webkit-scrollbar { background: transparent; max-width: 8px; + max-height: 6px; } ::-webkit-scrollbar-thumb { diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index cbde062604..090f733ce6 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -1,142 +1,157 @@ 'use client'; -import { Icon, type IconName } from '@gitbook/icons'; +import type { IconName } from '@gitbook/icons'; import * as NavigationMenu from '@radix-ui/react-navigation-menu'; import React from 'react'; -import { Link } from '@/components/primitives'; +import { Button, DropdownChevron, Link } from '@/components/primitives'; import { tcls } from '@/lib/tailwind'; +import { useIsMobile } from '../hooks/useIsMobile'; +import { CONTAINER_STYLE } from '../layout'; import { SectionIcon } from './SectionIcon'; import type { ClientSiteSection, ClientSiteSections } from './encodeClientSiteSections'; -const VIEWPORT_ITEM_WIDTH = 240; /* width of the tile (w-60) */ +const SCREEN_OFFSET = 16; // 1rem +const VIEWPORT_PADDING = 8; // 0.5rem const MIN_ITEMS_FOR_COLS = 4; /* number of items to switch to 2 columns */ /** * A set of navigational links representing site sections for multi-section sites */ -export function SiteSectionTabs(props: { sections: ClientSiteSections }) { +export function SiteSectionTabs(props: { + sections: ClientSiteSections; + className?: string; + children?: React.ReactNode; +}) { const { sections: { list: sectionsAndGroups, current: currentSection }, + className, + children, } = props; - const [value, setValue] = React.useState(); + + const currentTriggerRef = React.useRef(null); const [offset, setOffset] = React.useState(null); - const menuContainerRef = React.useRef(null); + const [value, setValue] = React.useState(undefined); - const onNodeUpdate = (trigger: HTMLButtonElement | null, itemValue: string, size = 0) => { - const padding = 16; - const margin = -12; // Offsetting the menu container's negative margin - const windowWidth = document.documentElement.clientWidth; - const windowBuffer = 16; // constrain to within the window with some buffer on the left and right we don't want the menu to enter - const viewportWidth = - size < MIN_ITEMS_FOR_COLS - ? VIEWPORT_ITEM_WIDTH + padding - : VIEWPORT_ITEM_WIDTH * 2 + padding; - const minOffset = 0 - (menuContainerRef.current?.offsetLeft ?? 0) + margin; - const maxOffset = minOffset + windowWidth - viewportWidth; + const isMobile = useIsMobile(768); - if (windowWidth < 768) { - // if the screen is small don't offset the menu - setOffset(minOffset + windowBuffer); - } else if (trigger && value === itemValue) { - const position = minOffset + trigger?.getBoundingClientRect().left; - setOffset( - Math.min(maxOffset - windowBuffer, Math.max(minOffset + windowBuffer, position)) - ); - } else if (!value) { - setOffset(null); + React.useEffect(() => { + const trigger = currentTriggerRef.current; + if (!trigger) { + return; } - }; + + const triggerWidth = trigger.getBoundingClientRect().width; + const triggerLeft = trigger.getBoundingClientRect().left; + setOffset(triggerLeft + triggerWidth / 2); + }); return sectionsAndGroups.length > 0 ? (
- - {sectionsAndGroups.map((sectionOrGroup) => { - const { id, title, icon } = sectionOrGroup; - const isGroup = sectionOrGroup.object === 'site-section-group'; - const isActiveGroup = - isGroup && - Boolean( - sectionOrGroup.sections.find((s) => s.id === currentSection.id) - ); - const isActive = isActiveGroup || id === currentSection.id; - return ( - - {isGroup ? ( - sectionOrGroup.sections.length > 0 ? ( - <> - - onNodeUpdate( - node, - id, - sectionOrGroup.sections.length - ) - } - asChild - onClick={(e) => { - if (value) { - e.preventDefault(); - e.stopPropagation(); - } - }} - > - - - - - - - ) : null - ) : ( - - - - )} - - ); - })} - + -
- - + {sectionsAndGroups.map((sectionOrGroup) => { + const { id, title, icon } = sectionOrGroup; + const isGroup = sectionOrGroup.object === 'site-section-group'; + const isActiveGroup = + isGroup && + Boolean( + sectionOrGroup.sections.find((s) => s.id === currentSection.id) + ); + const isActive = isActiveGroup || id === currentSection.id; + return ( + + {isGroup ? ( + sectionOrGroup.sections.length > 0 ? ( + <> + { + // Prevent clicking the trigger from closing when the viewport is open + if (value === id) { + e.preventDefault(); + e.stopPropagation(); + } + }} + > + + + + + + + ) : null + ) : ( + + + + )} + + ); + })} + +
+ + {children}
+
@@ -145,76 +160,34 @@ export function SiteSectionTabs(props: { sections: ClientSiteSections }) { } /** - * A tab representing a section + * A tab representing a section or section group */ const SectionTab = React.forwardRef(function SectionTab( - props: { isActive: boolean; title: string; icon?: IconName; url: string }, + props: { isActive: boolean; title: string; icon?: IconName; url?: string }, ref: React.Ref ) { const { isActive, title, icon, url, ...rest } = props; return ( - : null} + label={title} + trailing={!url ? : null} + active={isActive} className={tcls( - 'group relative my-2 flex select-none items-center justify-between circular-corners:rounded-full rounded-corners:rounded-xs px-3 py-1', + 'group/dropdown relative my-2 overflow-visible px-3 py-1', isActive - ? 'text-primary-subtle' - : 'text-tint hover:bg-tint-hover hover:text-tint-strong' + ? 'after:contents-[] after:-bottom-2 bg-transparent text-primary-subtle after:absolute after:inset-x-3 after:h-0.5 after:bg-primary-9' + : '' )} href={url} - > - - {icon ? : null} - {title} - - {isActive ? : null} - - ); -}); - -/** - * A tab representing a section group - */ -const SectionGroupTab = React.forwardRef(function SectionGroupTab( - props: { isActive: boolean; title: string; icon?: IconName }, - ref: React.Ref -) { - const { isActive, title, icon, ...rest } = props; - return ( - + /> ); }); -/** - * Horizontal line indicating the active tab - */ -function ActiveTabIndicator() { - return ( - - ); -} - /** * A list of section tiles grouped in the dropdown for a section group */ @@ -226,7 +199,7 @@ function SectionGroupTileList(props: { return (
    diff --git a/packages/gitbook/src/components/primitives/Button.tsx b/packages/gitbook/src/components/primitives/Button.tsx index 24af6ff9ce..8900c3f315 100644 --- a/packages/gitbook/src/components/primitives/Button.tsx +++ b/packages/gitbook/src/components/primitives/Button.tsx @@ -41,10 +41,14 @@ export const variantClasses = { 'text-tint', 'border-0', 'shadow-none!', + 'translate-y-0!', 'hover:bg-tint-hover', 'hover:text-tint-strong', + 'focus-visible:bg-tint-hover', + 'focus-visible:text-tint-strong', + 'data-[state=open]:bg-tint-hover', + 'data-[state=open]:text-tint-strong', 'contrast-more:bg-tint-subtle', - 'hover:depth-subtle:translate-y-0', 'disabled:text-tint/8', 'disabled:bg-transparent', ], @@ -82,7 +86,7 @@ export const variantClasses = { const activeClasses = { primary: 'bg-primary-solid-hover', - blank: 'bg-primary-active disabled:bg-primary-active text-primary-strong font-medium hover:text-primary-strong disabled:text-primary-strong hover:bg-primary-active', + blank: 'bg-primary-active disabled:bg-primary-active text-primary-strong font-medium hover:text-primary-strong disabled:text-primary-strong hover:bg-primary-active focus-visible:bg-primary-active focus-visible:text-primary-strong data-[state=open]:bg-primary-active data-[state=open]:text-primary-strong', secondary: 'bg-tint-active disabled:bg-tint-active', header: 'bg-header-link/3', }; From bb29763eb8736986e3d0f8def276192062fe97f9 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 16 Sep 2025 12:04:16 +0200 Subject: [PATCH 2/4] Simplify --- .../gitbook/src/components/Header/Header.tsx | 2 +- .../src/components/Header/SpacesDropdown.tsx | 4 +- .../SiteSections/SiteSectionTabs.tsx | 145 +++++++++--------- 3 files changed, 73 insertions(+), 78 deletions(-) diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index fbcba97090..9cfbcc113c 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -166,7 +166,7 @@ export function Header(props: {
    {withVariants === 'translations' ? ( -
    +
    diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index 090f733ce6..93d132d3ab 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -47,92 +47,86 @@ export function SiteSectionTabs(props: { return sectionsAndGroups.length > 0 ? (
    -
    - - {sectionsAndGroups.map((sectionOrGroup) => { - const { id, title, icon } = sectionOrGroup; - const isGroup = sectionOrGroup.object === 'site-section-group'; - const isActiveGroup = - isGroup && - Boolean( - sectionOrGroup.sections.find((s) => s.id === currentSection.id) - ); - const isActive = isActiveGroup || id === currentSection.id; - return ( - - {isGroup ? ( - sectionOrGroup.sections.length > 0 ? ( - <> - { - // Prevent clicking the trigger from closing when the viewport is open - if (value === id) { - e.preventDefault(); - e.stopPropagation(); - } - }} - > - - - - - - - ) : null - ) : ( - - - - )} - + {sectionsAndGroups.map((sectionOrGroup) => { + const { id, title, icon } = sectionOrGroup; + const isGroup = sectionOrGroup.object === 'site-section-group'; + const isActiveGroup = + isGroup && + Boolean( + sectionOrGroup.sections.find((s) => s.id === currentSection.id) ); - })} - -
    - - {children} + const isActive = isActiveGroup || id === currentSection.id; + return ( + + {isGroup ? ( + sectionOrGroup.sections.length > 0 ? ( + <> + { + // Prevent clicking the trigger from closing when the viewport is open + if (value === id) { + e.preventDefault(); + e.stopPropagation(); + } + }} + > + + + + + + + ) : null + ) : ( + + + + )} + + ); + })} +
    + {children} +
    ) { const { isActive, title, icon, url, ...rest } = props; + const isGroup = url === undefined; return (