diff --git a/.changeset/sweet-hornets-change.md b/.changeset/sweet-hornets-change.md new file mode 100644 index 0000000000..a5ae91aa82 --- /dev/null +++ b/.changeset/sweet-hornets-change.md @@ -0,0 +1,5 @@ +--- +'gitbook': minor +--- + +Revamp mobile navigation diff --git a/packages/gitbook/src/components/Header/DropdownMenu.tsx b/packages/gitbook/src/components/Header/DropdownMenu.tsx index 8f51e4685c..caa506c358 100644 --- a/packages/gitbook/src/components/Header/DropdownMenu.tsx +++ b/packages/gitbook/src/components/Header/DropdownMenu.tsx @@ -7,6 +7,7 @@ import { useState } from 'react'; import { type ClassValue, tcls } from '@/lib/tailwind'; import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu'; +import { Slot } from '@radix-ui/react-slot'; import { Link, type LinkInsightsProps } from '../primitives'; @@ -25,13 +26,21 @@ export function DropdownMenu(props: { children: React.ReactNode; /** Custom styles */ className?: ClassValue; - /** Open the dropdown on hover */ + /** Open the dropdown on hover + * @default false + */ openOnHover?: boolean; + /** Whether to render the dropdown menu in a portal + * @default true + */ + withPortal?: boolean; }) { - const { button, children, className, openOnHover = false } = props; + const { button, children, className, openOnHover = false, withPortal = true } = props; const [hovered, setHovered] = useState(false); const [clicked, setClicked] = useState(false); + const Portal = withPortal ? RadixDropdownMenu.Portal : Slot; + return ( - + setHovered(true)} onMouseLeave={() => setHovered(false)} align="start" - className="z-40 animate-present pt-2" + sideOffset={8} + className="z-40 animate-present" >
- + ); } diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index 304e853139..6c57909d16 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -11,7 +11,7 @@ import { HeaderLink } from './HeaderLink'; import { HeaderLinkMore } from './HeaderLinkMore'; import { HeaderLinks } from './HeaderLinks'; import { HeaderLogo } from './HeaderLogo'; -import { HeaderMobileMenu } from './HeaderMobileMenu'; +import { HeaderMobileMenuButton } from './HeaderMobileMenuButton'; import { SpacesDropdown } from './SpacesDropdown'; /** @@ -76,7 +76,7 @@ export function Header(props: { context: GitBookSiteContext; withTopHeader?: boo 'min-w-0 shrink items-center justify-start gap-2 lg:gap-4' )} > - >) { - const language = useLanguage(); - - const pathname = usePathname(); - const hasScrollRef = useRef(false); - - const toggleNavigation = () => { - if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) { - document.body.classList.remove(globalClassName); - } else { - document.body.classList.add(globalClassName); - window.scrollTo(0, 0); - } - }; - - const windowRef = useRef(typeof window === 'undefined' ? null : window); - useScrollListener(() => { - hasScrollRef.current = window.scrollY >= SCROLL_DISTANCE; - }, windowRef); - - // Close the navigation when navigating to a page - useEffect(() => { - document.body.classList.remove(globalClassName); - }, [pathname]); - - return ( - - ); -} diff --git a/packages/gitbook/src/components/Header/HeaderMobileMenuButton.tsx b/packages/gitbook/src/components/Header/HeaderMobileMenuButton.tsx new file mode 100644 index 0000000000..99609a46d3 --- /dev/null +++ b/packages/gitbook/src/components/Header/HeaderMobileMenuButton.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { Icon } from '@gitbook/icons'; + +import { useMobileMenuSheet } from '@/components/MobileMenu/useMobileMenuSheet'; +import { tString, useLanguage } from '@/intl/client'; +import { tcls } from '@/lib/tailwind'; + +/** + * Button to show/hide the table of content on mobile. + */ +export function HeaderMobileMenuButton( + props: Partial> +) { + const language = useLanguage(); + const { open, setOpen } = useMobileMenuSheet(); + + const toggleNavigation = () => { + setOpen(!open); + }; + + return ( + + ); +} diff --git a/packages/gitbook/src/components/Header/SpacesDropdown.tsx b/packages/gitbook/src/components/Header/SpacesDropdown.tsx index dd6887d955..8e5cc46aab 100644 --- a/packages/gitbook/src/components/Header/SpacesDropdown.tsx +++ b/packages/gitbook/src/components/Header/SpacesDropdown.tsx @@ -11,8 +11,9 @@ export function SpacesDropdown(props: { siteSpace: SiteSpace; siteSpaces: SiteSpace[]; className?: string; + withPortal?: boolean; }) { - const { context, siteSpace, siteSpaces, className } = props; + const { context, siteSpace, siteSpaces, className, withPortal } = props; return ( { + setOpen(false); + }, [pathname]); + + useEffect(() => { + // If the menu is open, we add a class to the body to prevent scrolling + if (open) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + } + }, [open]); + + return null; +} diff --git a/packages/gitbook/src/components/MobileMenu/index.ts b/packages/gitbook/src/components/MobileMenu/index.ts new file mode 100644 index 0000000000..e9ea4d0ce0 --- /dev/null +++ b/packages/gitbook/src/components/MobileMenu/index.ts @@ -0,0 +1,2 @@ +export * from './useMobileMenuSheet'; +export * from './MobileMenuScript'; diff --git a/packages/gitbook/src/components/MobileMenu/useMobileMenuSheet.ts b/packages/gitbook/src/components/MobileMenu/useMobileMenuSheet.ts new file mode 100644 index 0000000000..8de4d1071f --- /dev/null +++ b/packages/gitbook/src/components/MobileMenu/useMobileMenuSheet.ts @@ -0,0 +1,12 @@ +import { create } from 'zustand'; + +/** + * Hooks to manage the mobile menu sheet state. + */ +export const useMobileMenuSheet = create<{ + open: boolean; + setOpen: (open: boolean) => void; +}>((set) => ({ + open: false, + setOpen: (open) => set({ open }), +})); diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index cedc2eee89..4986a8de2e 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -5,13 +5,13 @@ import React from 'react'; import { Footer } from '@/components/Footer'; import { Header, HeaderLogo } from '@/components/Header'; import { SearchButton, SearchModal } from '@/components/Search'; -import { TableOfContents } from '@/components/TableOfContents'; +import { TOCScrollContent, TableOfContents } from '@/components/TableOfContents'; import { CONTAINER_STYLE } from '@/components/layout'; import { getSpaceLanguage } from '@/intl/server'; import { t } from '@/intl/translate'; +import type { VisitorAuthClaims } from '@/lib/adaptive'; import { tcls } from '@/lib/tailwind'; -import type { VisitorAuthClaims } from '@/lib/adaptive'; import { GITBOOK_API_PUBLIC_URL, GITBOOK_APP_URL } from '@v2/lib/env'; import { Announcement } from '../Announcement'; import { SpacesDropdown } from '../Header/SpacesDropdown'; @@ -81,7 +81,6 @@ export function SpaceLayout(props: { )} > ) } - innerHeader={ - // displays the search button and/or the space dropdown in the ToC according to the header/variant settings. E.g if there is no header, the search button will be displayed in the ToC. - <> - {!withTopHeader && ( -
- - - - {t( - getSpaceLanguage(customization), - customization.aiSearch.enabled - ? 'search_or_ask' - : 'search' - )} - ... - - - -
- )} - {!withTopHeader && withSections && sections && ( - - )} - {isMultiVariants && ( - + + {!withTopHeader && ( +
+ + + + {t( + getSpaceLanguage(customization), + customization.aiSearch.enabled + ? 'search_or_ask' + : 'search' + )} + ... + + + +
)} - /> - )} - - } - /> + {!withTopHeader && withSections && sections && ( + + )} + {isMultiVariants && ( + + )} + + ) : null + } + /> +
{children}
diff --git a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx index 24ea366a2e..60a400ca40 100644 --- a/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageGroupItem.tsx @@ -29,8 +29,8 @@ export function PageGroupItem(props: { '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle', '[html.sidebar-filled.theme-muted_&]:bg-tint-base', '[html.sidebar-filled.theme-bold.tint_&]:bg-tint-base', - '[html.sidebar-default.theme-gradient_&]:bg-gradient-primary', - '[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint' + 'lg:[html.sidebar-default.theme-gradient_&]:bg-gradient-primary', + 'lg:[html.sidebar-default.theme-gradient.tint_&]:bg-gradient-tint' )} > diff --git a/packages/gitbook/src/components/TableOfContents/TOCScrollContent.tsx b/packages/gitbook/src/components/TableOfContents/TOCScrollContent.tsx new file mode 100644 index 0000000000..875eedc908 --- /dev/null +++ b/packages/gitbook/src/components/TableOfContents/TOCScrollContent.tsx @@ -0,0 +1,69 @@ +import { PagesList } from '@/components/TableOfContents'; +import { Trademark } from '@/components/TableOfContents'; +import { TOCScrollContainer } from '@/components/TableOfContents/TOCScroller'; +import { tcls } from '@/lib/tailwind'; +import { SiteInsightsTrademarkPlacement } from '@gitbook/api'; +import type { GitBookSiteContext } from '@v2/lib/context'; + +export function TOCScrollContent(props: { + context: GitBookSiteContext; + innerHeader?: React.ReactNode; +}) { + const { context, innerHeader } = props; + const { customization } = context; + + return ( +
+ {!!innerHeader && ( +
+ {innerHeader} +
+ )} + + + + {customization.trademark.enabled ? ( + + ) : null} + +
+ ); +} diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 7ca7a2c0c0..31cccc42f1 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -1,39 +1,73 @@ -import { SiteInsightsTrademarkPlacement } from '@gitbook/api'; -import type { GitBookSiteContext } from '@v2/lib/context'; -import type React from 'react'; +'use client'; +import { MobileMenuScript, useMobileMenuSheet } from '@/components/MobileMenu'; +import { Button } from '@/components/primitives'; import { tcls } from '@/lib/tailwind'; - -import { PagesList } from './PagesList'; -import { TOCScrollContainer } from './TOCScroller'; +import type React from 'react'; import { TableOfContentsScript } from './TableOfContentsScript'; -import { Trademark } from './Trademark'; export function TableOfContents(props: { - context: GitBookSiteContext; header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header - innerHeader?: React.ReactNode; // Displayed outside the scrollable TOC, directly above the page list + children: React.ReactNode; }) { - const { innerHeader, context, header } = props; - const { space, customization, pages } = context; + const { header, children } = props; + const { open, setOpen } = useMobileMenuSheet(); return ( <> +
setOpen(false)} + /> + ); } diff --git a/packages/gitbook/src/components/TableOfContents/Trademark.tsx b/packages/gitbook/src/components/TableOfContents/Trademark.tsx index aa2d37c0ac..fe0eddedfb 100644 --- a/packages/gitbook/src/components/TableOfContents/Trademark.tsx +++ b/packages/gitbook/src/components/TableOfContents/Trademark.tsx @@ -21,13 +21,13 @@ export function Trademark(props: { return (