From 9e669f162a7c611c4b9c03bce1374a34f5b872d0 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 2 Dec 2025 10:09:24 +0100 Subject: [PATCH 01/20] Add SideSheet component, refactor TOC and AIChat to use it Also fixes RND-7803 --- .../gitbook/src/components/AIChat/AIChat.tsx | 16 +- .../src/components/Cookies/CookiesToast.tsx | 8 +- .../gitbook/src/components/Header/Header.tsx | 5 +- .../components/Header/HeaderMobileMenu.tsx | 7 +- .../RootLayout/CustomizationRootLayout.tsx | 3 +- .../src/components/SiteLayout/SiteLayout.tsx | 1 + .../components/SpaceLayout/SpaceLayout.tsx | 2 +- .../TableOfContents/PageGroupItem.tsx | 4 +- .../TableOfContents/TableOfContents.tsx | 42 +++-- packages/gitbook/src/components/layout.ts | 6 +- .../src/components/primitives/SideSheet.tsx | 165 ++++++++++++++++++ packages/gitbook/tailwind.config.ts | 22 ++- 12 files changed, 227 insertions(+), 54 deletions(-) create mode 100644 packages/gitbook/src/components/primitives/SideSheet.tsx diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index aa3ca9787e..cdef68278d 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -26,6 +26,7 @@ import { useTrackEvent } from '../Insights'; import { useNow } from '../hooks'; import { Button } from '../primitives'; import { ScrollContainer } from '../primitives/ScrollContainer'; +import { SideSheet } from '../primitives/SideSheet'; import { AIChatControlButton } from './AIChatControlButton'; import { AIChatIcon } from './AIChatIcon'; import { AIChatInput } from './AIChatInput'; @@ -68,16 +69,17 @@ export function AIChat() { }, [chat.opened, trackEvent]); return ( -
chatController.close()} data-testid="ai-chat" + withShim={true} className={tcls( - 'ai-chat inset-y-0 right-0 z-40 mx-auto flex max-w-3xl scroll-mt-36 px-4 py-4 transition-[width,opacity,margin,display] transition-discrete duration-300 sm:px-6 lg:fixed lg:w-80 lg:p-0 xl:w-96', - chat.opened - ? 'lg:starting:ml-0 lg:starting:w-0 lg:starting:opacity-0' - : 'hidden lg:ml-0 lg:w-0! lg:opacity-0' + 'ai-chat z-40 mx-auto not-hydrated:hidden w-96 max-w-full pl-8 transition-[width] duration-300 ease-quint lg:max-xl:w-80' )} > - + @@ -102,7 +104,7 @@ export function AIChat() { -
+ ); } diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx index caba3a2cfa..3de0c2b47e 100644 --- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx +++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx @@ -41,7 +41,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) { aria-describedby={describedById} className={tcls( 'fixed', - 'z-10', + 'z-50', 'bg-tint-base', 'rounded-sm', 'straight-corners:rounded-none', @@ -52,9 +52,9 @@ export function CookiesToast(props: { privacyPolicy?: string }) { 'depth-flat:shadow-none', 'p-4', 'pr-8', - 'bottom-4', - 'right-4', - 'left-16', + 'bottom-[max(env(safe-area-inset-bottom),1rem)]', + 'right-[max(env(safe-area-inset-right),1rem)]', + 'left-[max(env(safe-area-inset-left),4rem)]', 'max-w-md', 'text-balance', 'sm:left-auto', diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index 89d6cce7a0..8caf04ade5 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -42,11 +42,12 @@ export function Header(props: { `h-[${HEADER_HEIGHT_DESKTOP}px]`, 'sticky', 'top-0', + 'pt-[env(safe-area-inset-top)]', 'z-30', 'w-full', 'flex-none', - 'shadow-[0px_1px_0px]', - 'shadow-tint-12/2', + 'border-b', + 'border-tint-subtle', 'bg-tint-base/9', 'theme-muted:bg-tint-subtle/9', diff --git a/packages/gitbook/src/components/Header/HeaderMobileMenu.tsx b/packages/gitbook/src/components/Header/HeaderMobileMenu.tsx index 3b0ed49dfc..652081c636 100644 --- a/packages/gitbook/src/components/Header/HeaderMobileMenu.tsx +++ b/packages/gitbook/src/components/Header/HeaderMobileMenu.tsx @@ -23,12 +23,7 @@ export function HeaderMobileMenu(props: Partial { - if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) { - document.body.classList.remove(globalClassName); - } else { - document.body.classList.add(globalClassName); - window.scrollTo(0, 0); - } + document.body.classList.toggle(globalClassName); }; const windowRef = useRef(typeof window === 'undefined' ? null : window); diff --git a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx index be6dd59815..edaa0b0374 100644 --- a/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx +++ b/packages/gitbook/src/components/RootLayout/CustomizationRootLayout.tsx @@ -91,6 +91,7 @@ export async function CustomizationRootLayout(props: { suppressHydrationWarning lang={customization.internationalization.locale} className={tcls( + 'gutter-stable', customization.styling.corners && `${customization.styling.corners}-corners`, 'theme' in customization.styling && `theme-${customization.styling.theme}`, tintColor ? ' tint' : 'no-tint', @@ -179,7 +180,7 @@ export async function CustomizationRootLayout(props: { } `} - + diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 632135a283..5ad72bd96d 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -3,12 +3,16 @@ import { SiteInsightsTrademarkPlacement } from '@gitbook/api'; import type React from 'react'; import { tcls } from '@/lib/tailwind'; +import { SideSheet } from '../primitives/SideSheet'; import { PagesList } from './PagesList'; import { TOCScrollContainer } from './TOCScroller'; import { TableOfContentsScript } from './TableOfContentsScript'; import { Trademark } from './Trademark'; import { encodeClientTableOfContents } from './encodeClientTableOfContents'; +/** + * Sidebar container, responsible for setting the right dimensions and position for the sidebar. + */ export async function TableOfContents(props: { context: GitBookSiteContext; header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header @@ -21,23 +25,33 @@ export async function TableOfContents(props: { return ( <> - + ); diff --git a/packages/gitbook/src/components/layout.ts b/packages/gitbook/src/components/layout.ts index 4f6579ab6a..93dad216bd 100644 --- a/packages/gitbook/src/components/layout.ts +++ b/packages/gitbook/src/components/layout.ts @@ -9,9 +9,9 @@ export const HEADER_HEIGHT_DESKTOP = 64 as const; * Style for the container to adapt between normal and full width. */ export const CONTAINER_STYLE: ClassValue = [ - 'px-4', - 'sm:px-6', - 'md:px-8', + 'px-4 pl-[max(env(safe-area-inset-left),1rem)] pr-[max(env(safe-area-inset-right),1rem)]', + 'sm:px-6 sm:pl-[max(env(safe-area-inset-left),1.5rem)] sm:pr-[max(env(safe-area-inset-right),1.5rem)]', + 'md:px-8 md:pl-[max(env(safe-area-inset-left),2rem)] md:pr-[max(env(safe-area-inset-right),2rem)]', 'max-w-screen-2xl', 'mx-auto', ]; diff --git a/packages/gitbook/src/components/primitives/SideSheet.tsx b/packages/gitbook/src/components/primitives/SideSheet.tsx new file mode 100644 index 0000000000..de332aface --- /dev/null +++ b/packages/gitbook/src/components/primitives/SideSheet.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { useLanguage } from '@/intl/client'; +import { tString } from '@/intl/translate'; +import { type ClassValue, tcls } from '@/lib/tailwind'; +import React from 'react'; +import { useIsMobile } from '../hooks/useIsMobile'; +import { Button } from './Button'; + +export function SideSheet( + props: { + side: 'left' | 'right'; + open?: boolean; + toggleClass?: string; + modal?: true | false | 'mobile'; + onClose?: () => void; + withShim?: boolean; + withCloseButton?: boolean; + } & React.HTMLAttributes +) { + const { + side, + children, + className, + toggleClass, + open: openState, + modal = 'mobile', + withShim, + withCloseButton, + onClose, + ...rest + } = props; + + const isMobile = useIsMobile(); + const isModal = modal === 'mobile' ? isMobile : modal; + + const [open, setOpen] = React.useState(openState ?? false); + + // Use prop if provided (controlled), otherwise use internal state (uncontrolled) + const isOpen = openState !== undefined ? openState : open; + + const handleClose = React.useCallback(() => { + if (openState !== undefined) { + // Controlled mode: notify parent + onClose?.(); + } else { + // Uncontrolled mode: update internal state + setOpen(false); + if (toggleClass) { + document.body.classList.remove(toggleClass); + } + } + }, [openState, onClose, toggleClass]); + + React.useEffect(() => { + if (!toggleClass) { + return; + } + + const callback = (mutationList: MutationRecord[]) => { + for (const mutation of mutationList) { + if (mutation.attributeName === 'class') { + const shouldBeOpen = document.body.classList.contains(toggleClass); + if (openState !== undefined) { + // Controlled mode: notify parent if state should change + if (shouldBeOpen !== openState) { + if (shouldBeOpen) { + // Opening via class - no callback, just sync + // Parent should handle this via toggleClass observation + } else { + onClose?.(); + } + } + } else { + // Uncontrolled mode: update internal state + setOpen(shouldBeOpen); + } + } + } + }; + + const observer = new MutationObserver(callback); + observer.observe(document.body, { attributes: true }); + + return () => observer.disconnect(); + }, [toggleClass, openState, onClose]); + + return ( + <> + {isModal && withShim ? ( + + ) : null} + + + ); +} + +export function SideSheetShim(props: { className?: ClassValue; onClick?: () => void }) { + const { className, onClick } = props; + return ( +
{ + onClick?.(); + }} + onKeyUp={(e) => { + if (e.key === 'Escape') { + onClick?.(); + } + }} + className={tcls( + 'fixed inset-0 z-40 items-start bg-tint-base/3 not-hydrated:opacity-0 starting:opacity-0 backdrop-blur-md transition-[opacity,display,filter] transition-discrete duration-250', + className + )} + /> + ); +} + +export function SideSheetCloseButton(props: { className?: ClassValue; onClick?: () => void }) { + const { className, onClick } = props; + const language = useLanguage(); + return ( +
From 9281b5e3b7d3aee053efda940a4e4df5805360da Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 11 Dec 2025 14:26:46 +0100 Subject: [PATCH 14/20] Update SideSheet.tsx --- packages/gitbook/src/components/primitives/SideSheet.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/src/components/primitives/SideSheet.tsx b/packages/gitbook/src/components/primitives/SideSheet.tsx index e90fd43da1..7e5b88464a 100644 --- a/packages/gitbook/src/components/primitives/SideSheet.tsx +++ b/packages/gitbook/src/components/primitives/SideSheet.tsx @@ -127,9 +127,9 @@ export function SideSheet( : side === 'left' ? 'hydrated:animate-exit-to-left' : 'hydrated:animate-exit-to-right', - 'fixed inset-y-0 z-41 max-w-full', // Above the side sheet scrim on z-40 + 'fixed inset-y-0 z-41', // Above the side sheet scrim on z-40 side === 'left' ? 'left-0' : 'right-0', - withCloseButton ? (side === 'left' ? 'mr-16' : 'ml-16') : '', + withCloseButton ? 'max-w-[calc(100vw-5rem)]' : 'max-w-[calc(100vw-3rem)]', isOpen ? '' : 'hidden', className )} From dcd03554bd5d52028e310177739cb2198ea8ba4b Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 11 Dec 2025 14:28:26 +0100 Subject: [PATCH 15/20] Update TOC to use normal scrollcontainer --- .../gitbook/src/components/AIChat/AIChat.tsx | 4 +- .../SiteSections/SiteSectionList.tsx | 2 +- .../SiteSections/SiteSectionTabs.tsx | 2 +- .../TableOfContents/TOCScroller.tsx | 91 ------------------- .../TableOfContents/TableOfContents.tsx | 27 +++--- .../TableOfContents/ToggleableLinkItem.tsx | 5 +- .../components/TableOfContents/Trademark.tsx | 28 +----- .../src/components/TableOfContents/index.ts | 1 - .../components/primitives/ScrollContainer.tsx | 12 ++- 9 files changed, 28 insertions(+), 144 deletions(-) delete mode 100644 packages/gitbook/src/components/TableOfContents/TOCScroller.tsx diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index d36ebe0e9d..a5470d61db 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -83,7 +83,7 @@ export function AIChat() { data-testid="ai-chat" withScrim={true} className={tcls( - 'ai-chat mx-auto not-hydrated:hidden w-96 max-w-full pl-8 transition-[width] duration-300 ease-quint lg:max-xl:w-80' + 'ai-chat mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:max-xl:w-80' )} > @@ -226,7 +226,7 @@ export function AIChatBody(props: { contentClassName="p-4 gutter-stable flex flex-col gap-4" orientation="vertical" fadeEdges={['leading']} - active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`} + active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`} > {isEmpty ? (
diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx index 5254a7606a..1b68da6439 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionList.tsx @@ -40,7 +40,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className orientation="vertical" style={{ maxHeight: `${MAX_ITEMS * 3 + 2}rem` }} className="pb-4" - active={currentSection.id} + active={`#${currentSection.id}`} >
{sectionsAndGroups.map((item) => { diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index e176a3f826..ede6ca5b1f 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -76,7 +76,7 @@ export function SiteSectionTabs(props: { ? 'md:-mr-8 -mr-4 sm:-mr-6' : 'after:contents[] after:absolute after:inset-y-2 after:right-0 after:border-transparent after:border-r after:transition-colors' )} - active={currentSection.id} + active={`#${currentSection.id}`} trailingEdgeScrollClassName={children ? 'after:border-tint' : ''} > void) => () => void; -} - -const TOCScrollContainerContext = React.createContext(null); - -function useTOCScrollContainerContext() { - const ctx = React.useContext(TOCScrollContainerContext); - assert(ctx); - return ctx; -} - -/** - * Table of contents scroll container. - */ -export function TOCScrollContainer(props: ComponentPropsWithoutRef<'div'>) { - const ref = useRef(null); - const listeners = useRef<((element: HTMLDivElement) => void)[]>([]); - const getContainer: TOCScrollContainerContextType['getContainer'] = useCallback((listener) => { - if (ref.current) { - listener(ref.current); - return () => {}; - } - listeners.current.push(listener); - return () => { - listeners.current = listeners.current.filter((l) => l !== listener); - }; - }, []); - const value: TOCScrollContainerContextType = useMemo(() => ({ getContainer }), [getContainer]); - useEffect(() => { - const element = ref.current; - if (!element) { - return; - } - listeners.current.forEach((listener) => listener(element)); - return () => { - listeners.current = []; - }; - }, []); - - return ( - -
- - ); -} - -// Offset to scroll the table of contents item by. -const TOC_ITEM_OFFSET = 100; - -/** - * Scrolls the table of contents container to the page item when it's initially active. - */ -export function useScrollToActiveTOCItem(props: { - anchorRef: React.RefObject; - isActive: boolean; -}) { - const { isActive, anchorRef } = props; - const { getContainer } = useTOCScrollContainerContext(); - useEffect(() => { - const anchor = anchorRef.current; - if (isActive && anchor) { - return getContainer((container) => { - if (isOutOfView(anchor, container)) { - container.scrollTo({ top: anchor.offsetTop - TOC_ITEM_OFFSET }); - } - }); - } - }, [isActive, getContainer, anchorRef]); -} - -function isOutOfView(element: HTMLElement, container: HTMLElement) { - const tocItemTop = element.offsetTop; - const containerTop = container.scrollTop; - const containerBottom = containerTop + container.clientHeight; - return ( - tocItemTop < containerTop + TOC_ITEM_OFFSET || - tocItemTop > containerBottom - TOC_ITEM_OFFSET - ); -} diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 85bce61584..4aaa978deb 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -3,9 +3,9 @@ import { SiteInsightsTrademarkPlacement } from '@gitbook/api'; import type React from 'react'; import { tcls } from '@/lib/tailwind'; +import { ScrollContainer } from '../primitives/ScrollContainer'; import { SideSheet } from '../primitives/SideSheet'; import { PagesList } from './PagesList'; -import { TOCScrollContainer } from './TOCScroller'; import { TableOfContentsScript } from './TableOfContentsScript'; import { Trademark } from './Trademark'; import { encodeClientTableOfContents } from './encodeClientTableOfContents'; @@ -74,7 +74,7 @@ export async function TableOfContents(props: { 'lg:page-no-toc:[html[style*="--outline-top-offset"]_&]:top-(--outline-top-offset)!', 'lg:page-no-toc:[html[style*="--outline-height"]_&]:top-(--outline-height)!', - 'py-6', + 'pt-6 pb-4', 'lg:sidebar-filled:pr-6', 'lg:page-no-toc:pr-0', 'max-lg:pl-8', @@ -106,21 +106,24 @@ export async function TableOfContents(props: { )} > {innerHeader ? innerHeader : null} - - {customization.trademark.enabled ? ( - - ) : null} - + + {customization.trademark.enabled ? ( + + ) : null}
diff --git a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx index 4d222da04f..a907cd3dd6 100644 --- a/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/ToggleableLinkItem.tsx @@ -8,7 +8,6 @@ import { tcls } from '@/lib/tailwind'; import { useCurrentPagePath } from '../hooks'; import { Link, type LinkInsightsProps, type LinkProps } from '../primitives'; -import { useScrollToActiveTOCItem } from './TOCScroller'; /** * Client component for a page document to toggle its children and be marked as active. @@ -81,8 +80,6 @@ function LinkItem( } ) { const { isActive, href, insights, children, onActiveClick } = props; - const anchorRef = useRef(null); - useScrollToActiveTOCItem({ anchorRef, isActive }); const handleClick = (event: React.MouseEvent) => { if (isActive && onActiveClick) { @@ -93,7 +90,7 @@ function LinkItem( return ( +
); diff --git a/packages/gitbook/src/components/TableOfContents/index.ts b/packages/gitbook/src/components/TableOfContents/index.ts index 6eeff92697..03420d7a00 100644 --- a/packages/gitbook/src/components/TableOfContents/index.ts +++ b/packages/gitbook/src/components/TableOfContents/index.ts @@ -1,4 +1,3 @@ export { TableOfContents } from './TableOfContents'; export { PagesList } from './PagesList'; -export { TOCScrollContainer } from './TOCScroller'; export { Trademark } from './Trademark'; diff --git a/packages/gitbook/src/components/primitives/ScrollContainer.tsx b/packages/gitbook/src/components/primitives/ScrollContainer.tsx index 0e6051f29e..aea4079a2e 100644 --- a/packages/gitbook/src/components/primitives/ScrollContainer.tsx +++ b/packages/gitbook/src/components/primitives/ScrollContainer.tsx @@ -102,7 +102,9 @@ export function ScrollContainer(props: ScrollContainerProps) { return; } const activeItem = - typeof active === 'string' ? document.getElementById(active) : active.current; + typeof active === 'string' + ? containerRef.current?.querySelector(active) + : active.current; if (!activeItem || !container.contains(activeItem)) { return; } @@ -180,7 +182,7 @@ export function ScrollContainer(props: ScrollContainerProps) { orientation === 'horizontal' ? '-translate-y-1/2! top-1/2 left-0 ml-2' : '-translate-x-1/2! top-0 left-1/2 mt-2', - 'absolute not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]', + 'absolute z-10 not-pointer-none:block hidden scale-0 opacity-0 transition-[scale,opacity]', scrollPosition > 0 ? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11' : 'pointer-events-none' @@ -199,7 +201,7 @@ export function ScrollContainer(props: ScrollContainerProps) { orientation === 'horizontal' ? '-translate-y-1/2! top-1/2 right-0 mr-2' : '-translate-x-1/2! bottom-0 left-1/2 mb-2', - 'absolute not-pointer-none:block hidden scale-0 transition-[scale,opacity]', + 'absolute z-10 not-pointer-none:block hidden scale-0 transition-[scale,opacity]', scrollPosition < scrollSize ? 'not-pointer-none:group-hover/scroll-container:scale-100 not-pointer-none:group-hover/scroll-container:opacity-11' : 'pointer-events-none' @@ -214,7 +216,7 @@ export function ScrollContainer(props: ScrollContainerProps) { /** * Scroll to an element in a container. */ -function scrollToElementInContainer(element: HTMLElement, container: HTMLElement) { +function scrollToElementInContainer(element: Element, container: HTMLElement) { const containerRect = container.getBoundingClientRect(); const rect = element.getBoundingClientRect(); @@ -229,6 +231,6 @@ function scrollToElementInContainer(element: HTMLElement, container: HTMLElement (rect.left - containerRect.left) - container.clientWidth / 2 + rect.width / 2, - behavior: 'smooth', + behavior: 'auto', }); } From 8a887fd89066c52d6e8e05213284629f892e3665 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 11 Dec 2025 14:39:00 +0100 Subject: [PATCH 16/20] Fix tests --- .../gitbook/src/components/TableOfContents/TableOfContents.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 4aaa978deb..66c7c5aa8c 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -107,6 +107,7 @@ export async function TableOfContents(props: { > {innerHeader ? innerHeader : null} Date: Thu, 11 Dec 2025 17:27:49 +0100 Subject: [PATCH 17/20] Rework table of contents to accommodate faded edges --- .../gitbook/src/components/AIChat/AIChat.tsx | 2 +- .../Embeddable/EmbeddableDocsPage.tsx | 2 +- .../SiteSections/SiteSectionTabs.tsx | 6 +- .../TableOfContents/PageGroupItem.tsx | 8 +- .../TableOfContents/TableOfContents.tsx | 13 +- .../components/TableOfContents/Trademark.tsx | 2 +- .../components/primitives/ScrollContainer.tsx | 138 ++++++++++-------- 7 files changed, 99 insertions(+), 72 deletions(-) diff --git a/packages/gitbook/src/components/AIChat/AIChat.tsx b/packages/gitbook/src/components/AIChat/AIChat.tsx index 74230a5c81..0daf45a4a1 100644 --- a/packages/gitbook/src/components/AIChat/AIChat.tsx +++ b/packages/gitbook/src/components/AIChat/AIChat.tsx @@ -229,7 +229,7 @@ export function AIChatBody(props: { className="shrink grow basis-80 animate-fade-in-slow [container-type:size]" contentClassName="p-4 gutter-stable flex flex-col gap-4" orientation="vertical" - fadeEdges={['leading']} + trailing={{ fade: false, button: true }} active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`} > {isEmpty ? ( diff --git a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx index 69fc08b495..6edc6cc838 100644 --- a/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx +++ b/packages/gitbook/src/components/Embeddable/EmbeddableDocsPage.tsx @@ -79,7 +79,7 @@ export async function EmbeddableDocsPage( orientation="vertical" className="not-hydrated:animate-blur-in-slow" contentClassName="p-4" - fadeEdges={context.sections ? [] : ['leading']} + leading={{ fade: !context.sections, button: true }} >
diff --git a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx index 66c7c5aa8c..66fa4e0a1f 100644 --- a/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx +++ b/packages/gitbook/src/components/TableOfContents/TableOfContents.tsx @@ -74,7 +74,7 @@ export async function TableOfContents(props: { 'lg:page-no-toc:[html[style*="--outline-top-offset"]_&]:top-(--outline-top-offset)!', 'lg:page-no-toc:[html[style*="--outline-height"]_&]:top-(--outline-height)!', - 'pt-6 pb-4', + 'pt-4 pb-4', 'lg:sidebar-filled:pr-6', 'lg:page-no-toc:pr-0', 'max-lg:pl-8', @@ -88,7 +88,7 @@ export async function TableOfContents(props: {
+
); diff --git a/packages/gitbook/src/components/primitives/ScrollContainer.tsx b/packages/gitbook/src/components/primitives/ScrollContainer.tsx index aea4079a2e..f5a5e132ed 100644 --- a/packages/gitbook/src/components/primitives/ScrollContainer.tsx +++ b/packages/gitbook/src/components/primitives/ScrollContainer.tsx @@ -4,7 +4,7 @@ import { tString, useLanguage } from '@/intl/client'; import { tcls } from '@/lib/tailwind'; import * as React from 'react'; import { useScrollListener } from '../hooks/useScrollListener'; -import { Button } from './Button'; +import { Button, type ButtonProps } from './Button'; /** * A container that encapsulates a scrollable area with usability features. @@ -17,17 +17,26 @@ export type ScrollContainerProps = { className?: string; contentClassName?: string; - /** Optional class(es) to apply when there the container can be scrolled on the leading (left or top) edge */ - leadingEdgeScrollClassName?: string; - - /** Optional class(es) to apply when there the container can be scrolled on the trailing (right or bottom) edge */ - trailingEdgeScrollClassName?: string; - /** The direction of the scroll container. */ orientation: 'horizontal' | 'vertical'; - /** Whether to fade out the edges of the container. */ - fadeEdges?: ('leading' | 'trailing')[]; + leading?: { + /** Whether to fade out the leading edge of the container. */ + fade: boolean; + /** Whether to show a button to scroll back. */ + button: boolean | ButtonProps; + /** Optional class(es) to apply when there the container can be scrolled on the leading (left or top) edge */ + className?: string; + }; + + trailing?: { + /** Whether to fade out the trailing edge of the container. */ + fade: boolean; + /** Whether to show a button to scroll forward. */ + button: boolean | ButtonProps; + /** Optional class(es) to apply when there the container can be scrolled on the trailing (right or bottom) edge */ + className?: string; + }; /** The ID or ref of the active item to scroll to. */ active?: string | React.RefObject; @@ -39,10 +48,9 @@ export function ScrollContainer(props: ScrollContainerProps) { className, contentClassName, orientation, - fadeEdges = ['leading', 'trailing'], active, - leadingEdgeScrollClassName, - trailingEdgeScrollClassName, + leading = { fade: true, button: true }, + trailing = { fade: true, button: true }, ...rest } = props; @@ -140,28 +148,30 @@ export function ScrollContainer(props: ScrollContainerProps) { return (
0 ? leadingEdgeScrollClassName : '', - scrollPosition < scrollSize ? trailingEdgeScrollClassName : '' + scrollPosition > 0 ? leading?.className : '', + scrollPosition < scrollSize ? trailing?.className : '' )} {...rest} > {/* Scrollable content */}
0 + leading.fade && scrollPosition > 0 ? orientation === 'horizontal' - ? 'mask-l-from-[calc(100%-2rem)]' - : 'mask-t-from-[calc(100%-2rem)]' + ? 'mask-l-from-[calc(100%-1.5rem)]' + : 'mask-t-from-[calc(100%-1.5rem)]' : '', - fadeEdges.includes('trailing') && scrollPosition < scrollSize + trailing.fade && scrollPosition < scrollSize ? orientation === 'horizontal' - ? 'mask-r-from-[calc(100%-2rem)]' - : 'mask-b-from-[calc(100%-2rem)]' + ? 'mask-r-from-[calc(100%-1.5rem)]' + : 'mask-b-from-[calc(100%-1.5rem)]' : '', contentClassName )} @@ -171,44 +181,52 @@ export function ScrollContainer(props: ScrollContainerProps) {
{/* Scroll buttons back & forward */} -
); } From 43e8deca0f65791b00e4416eec49ce873e4d9d96 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 11 Dec 2025 17:56:49 +0100 Subject: [PATCH 18/20] Add margin to first document item --- .../gitbook/src/components/TableOfContents/PageDocumentItem.tsx | 2 +- .../gitbook/src/components/TableOfContents/PageGroupItem.tsx | 2 +- .../gitbook/src/components/TableOfContents/PageLinkItem.tsx | 2 +- .../gitbook/src/components/TableOfContents/TableOfContents.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx index 3180e197e7..13695a0d4c 100644 --- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx @@ -12,7 +12,7 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) { const { page } = props; return ( -
  • +
  • +
  • +
  • {customization.trademark.enabled ? ( From 5347917a47bf9a5c9064552230840072b2a34451 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 11 Dec 2025 19:16:24 +0100 Subject: [PATCH 19/20] Spacing --- .../src/components/SpaceLayout/SpaceLayout.tsx | 2 +- .../components/TableOfContents/PageDocumentItem.tsx | 2 +- .../src/components/TableOfContents/PageGroupItem.tsx | 10 +++++----- .../src/components/TableOfContents/PageLinkItem.tsx | 2 +- .../components/TableOfContents/TableOfContents.tsx | 6 +++--- .../src/components/primitives/ScrollContainer.tsx | 12 ++++++------ 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index f50c466fc0..aaa3b30a5e 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -174,7 +174,7 @@ export function SpaceLayout(props: SpaceLayoutProps) { !withTopHeader || variants.generic.length > 1 ? (
    1 ? '' : 'max-lg:hidden' )} > diff --git a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx index 13695a0d4c..3180e197e7 100644 --- a/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageDocumentItem.tsx @@ -12,7 +12,7 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) { const { page } = props; return ( -
  • +
  • +
  • diff --git a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx index 43d2d22313..bf14adf9d0 100644 --- a/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx +++ b/packages/gitbook/src/components/TableOfContents/PageLinkItem.tsx @@ -13,7 +13,7 @@ export function PageLinkItem(props: { page: ClientTOCPageLink }) { const { page } = props; return ( -
  • +
  • {customization.trademark.enabled ? ( diff --git a/packages/gitbook/src/components/primitives/ScrollContainer.tsx b/packages/gitbook/src/components/primitives/ScrollContainer.tsx index f5a5e132ed..b57c043b6a 100644 --- a/packages/gitbook/src/components/primitives/ScrollContainer.tsx +++ b/packages/gitbook/src/components/primitives/ScrollContainer.tsx @@ -165,13 +165,13 @@ export function ScrollContainer(props: ScrollContainerProps) { orientation === 'horizontal' ? 'overflow-x-scroll' : 'flex-col overflow-y-auto', leading.fade && scrollPosition > 0 ? orientation === 'horizontal' - ? 'mask-l-from-[calc(100%-1.5rem)]' - : 'mask-t-from-[calc(100%-1.5rem)]' + ? 'mask-l-from-[calc(100%-1rem)]' + : 'mask-t-from-[calc(100%-1rem)]' : '', trailing.fade && scrollPosition < scrollSize ? orientation === 'horizontal' - ? 'mask-r-from-[calc(100%-1.5rem)]' - : 'mask-b-from-[calc(100%-1.5rem)]' + ? 'mask-r-from-[calc(100%-1rem)]' + : 'mask-b-from-[calc(100%-1rem)]' : '', contentClassName )} @@ -192,7 +192,7 @@ export function ScrollContainer(props: ScrollContainerProps) { label={tString(language, 'scroll_back')} {...(typeof leading.button === 'object' ? leading.button : {})} className={tcls( - 'bg-tint-base!', + 'z-10 bg-tint-base!', orientation === 'horizontal' ? '-translate-y-1/2! top-1/2 left-0 ml-2' : '-translate-x-1/2! top-0 left-1/2 mt-2', @@ -215,7 +215,7 @@ export function ScrollContainer(props: ScrollContainerProps) { label={tString(language, 'scroll_further')} {...(typeof trailing.button === 'object' ? trailing.button : {})} className={tcls( - 'bg-tint-base!', + 'z-10 bg-tint-base!', orientation === 'horizontal' ? '-translate-y-1/2! top-1/2 right-0 mr-2' : '-translate-x-1/2! bottom-0 left-1/2 mb-2', From aa86711d5aeec8eec6d92da85fef1aa899f566af Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 11 Dec 2025 19:33:38 +0100 Subject: [PATCH 20/20] More spacing --- packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index aaa3b30a5e..d5b50e0b0b 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -174,7 +174,7 @@ export function SpaceLayout(props: SpaceLayoutProps) { !withTopHeader || variants.generic.length > 1 ? (
    1 ? '' : 'max-lg:hidden' )} >