Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9e669f1
Add SideSheet component, refactor TOC and AIChat to use it
zenoachtig Dec 2, 2025
0d44191
Fix trademark & other visual bugs
zenoachtig Dec 2, 2025
f53e042
Naming and docs
zenoachtig Dec 2, 2025
e05f262
Get ready for supporting side sections list
zenoachtig Dec 2, 2025
a7b80c3
Small fixes
zenoachtig Dec 2, 2025
b9f9623
Fix scroll gutter
zenoachtig Dec 2, 2025
555a1da
Changeset
zenoachtig Dec 2, 2025
fb49a27
Layout tweaks
zenoachtig Dec 2, 2025
0a7979e
More layout fixes
zenoachtig Dec 2, 2025
72a0671
Layout
zenoachtig Dec 2, 2025
f78e9ca
Merge branch 'main' into zeno/rnd-7450-mobile-toc-chat-layout
zenoachtig Dec 4, 2025
8436352
Make sidesheet work well in docs embed
zenoachtig Dec 4, 2025
c895672
Update Header.tsx
zenoachtig Dec 4, 2025
e329b41
Merge branch 'main' into zeno/rnd-7450-mobile-toc-chat-layout
zenoachtig Dec 5, 2025
e372dae
Merge branch 'main' into zeno/rnd-7450-mobile-toc-chat-layout
zenoachtig Dec 11, 2025
ec0b607
Update AnnouncementBanner.tsx
zenoachtig Dec 11, 2025
9281b5e
Update SideSheet.tsx
zenoachtig Dec 11, 2025
dcd0355
Update TOC to use normal scrollcontainer
zenoachtig Dec 11, 2025
2f6ea5b
Merge branch 'main' into zeno/rnd-7450-mobile-toc-chat-layout
zenoachtig Dec 11, 2025
8a887fd
Fix tests
zenoachtig Dec 11, 2025
4cdab37
Rework table of contents to accommodate faded edges
zenoachtig Dec 11, 2025
43e8dec
Add margin to first document item
zenoachtig Dec 11, 2025
5347917
Spacing
zenoachtig Dec 11, 2025
aa86711
More spacing
zenoachtig Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/curly-eels-carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": patch
---

Add sidesheet component, use it for TOC and AIChat
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export default async function SiteDynamicLayout({

return (
<CustomizationRootLayout
className="site-background"
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
forcedTheme={forcedTheme}
context={context}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export default async function SiteStaticLayout({
const withTracking = shouldTrackEvents();

return (
<CustomizationRootLayout className="site-background" context={context}>
<CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable"
bodyClassName="site-background"
context={context}
>
<SiteLayout
context={context}
withTracking={withTracking}
Expand Down
26 changes: 17 additions & 9 deletions packages/gitbook/src/components/AIChat/AIChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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';
Expand Down Expand Up @@ -69,16 +70,23 @@ export function AIChat() {
}, [chat.opened, trackEvent]);

return (
<div
<SideSheet
side="right"
open={chat.opened}
onOpenChange={(open) => {
if (open) {
chatController.open();
} else {
chatController.close();
}
}}
data-testid="ai-chat"
withScrim={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 mx-auto ml-8 not-hydrated:hidden w-96 transition-[width] duration-300 ease-quint lg:max-xl:w-80'
)}
>
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base transition-all duration-300 max-lg:circular-corners:rounded-3xl max-lg:rounded-corners:rounded-md max-lg:border lg:w-80 xl:w-96">
<EmbeddableFrame className="relative shrink-0 border-tint-subtle border-l to-tint-base">
<EmbeddableFrameMain>
<EmbeddableFrameHeader>
<AIChatDynamicIcon trademark={config.trademark} />
Expand Down Expand Up @@ -109,7 +117,7 @@ export function AIChat() {
</EmbeddableFrameBody>
</EmbeddableFrameMain>
</EmbeddableFrame>
</div>
</SideSheet>
);
}

Expand Down Expand Up @@ -221,8 +229,8 @@ 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']}
active={`message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
trailing={{ fade: false, button: true }}
active={`#message-group-${chat.messages.filter((message) => message.role === 'user').length - 1}`}
>
{isEmpty ? (
<div className="flex grow flex-col">
Expand Down
122 changes: 62 additions & 60 deletions packages/gitbook/src/components/Announcement/AnnouncementBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,66 +35,68 @@ export function AnnouncementBanner(props: {
data-nosnippet=""
>
<div className="transition-all duration-300 lg:chat-open:pr-80 xl:chat-open:pr-96">
<div className={tcls('relative', CONTAINER_STYLE)}>
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
style.container,
closeable && 'pr-12',
hasLink && style.hover
)}
insights={
announcement.link
? {
type: 'link_click',
link: {
target: announcement.link.to,
position: SiteInsightsLinkPosition.Announcement,
},
}
: undefined
}
>
<Icon
icon={style.icon as IconName}
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
/>
<div>
{announcement.message}
{hasLink ? (
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
{contentRef?.icon ? (
<span className="mr-1 ml-2 *:inline">
{contentRef?.icon}
</span>
) : null}
{announcement.link?.title && (
<span className="mr-1">{announcement.link?.title}</span>
)}
<Icon
icon={
announcement.link?.to.kind === 'url'
? 'arrow-up-right'
: 'chevron-right'
}
className={tcls('mb-0.5 inline size-3')}
/>
</div>
) : null}
</div>
</Tag>
{closeable ? (
<Button
iconOnly
icon="close"
label={tString(language, 'close')}
variant="blank"
size="default"
onClick={dismissAnnouncement}
className={`absolute top-0 right-4 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 sm:right-6 md:right-8 ${style.close}`}
/>
) : null}
<div className={tcls(CONTAINER_STYLE)}>
<div className="relative">
<Tag
href={contentRef?.href ?? ''}
className={tcls(
'flex w-full items-start justify-center overflow-hidden circular-corners:rounded-xl rounded-md straight-corners:rounded-none px-4 py-3 text-neutral-strong text-sm theme-bold:ring-1 theme-gradient:ring-1 ring-inset transition-colors',
style.container,
closeable && 'pr-12',
hasLink && style.hover
)}
insights={
announcement.link
? {
type: 'link_click',
link: {
target: announcement.link.to,
position: SiteInsightsLinkPosition.Announcement,
},
}
: undefined
}
>
<Icon
icon={style.icon as IconName}
className={`mt-0.5 mr-3 size-4 shrink-0 ${style.iconColor}`}
/>
<div>
{announcement.message}
{hasLink ? (
<div className={tcls(LinkStyles, style.link, 'ml-1 inline')}>
{contentRef?.icon ? (
<span className="mr-1 ml-2 *:inline">
{contentRef?.icon}
</span>
) : null}
{announcement.link?.title && (
<span className="mr-1">{announcement.link?.title}</span>
)}
<Icon
icon={
announcement.link?.to.kind === 'url'
? 'arrow-up-right'
: 'chevron-right'
}
className={tcls('mb-0.5 inline size-3')}
/>
</div>
) : null}
</div>
</Tag>
{closeable ? (
<Button
iconOnly
icon="close"
label={tString(language, 'close')}
variant="blank"
size="default"
onClick={dismissAnnouncement}
className={`absolute top-0 right-0 mt-2 mr-2 circular-corners:rounded-lg rounded-sm straight-corners:rounded-none p-1.5 transition-all hover:ring-1 ${style.close}`}
/>
) : null}
</div>
</div>
</div>
</div>
Expand Down
8 changes: 4 additions & 4 deletions packages/gitbook/src/components/Cookies/CookiesToast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ 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 }}
>
<TableOfContents className="pt-0" context={context} />
<TableOfContents context={context} />
<PageBody
context={context}
page={page}
Expand Down
1 change: 1 addition & 0 deletions packages/gitbook/src/components/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function Header(props: {
`h-[${HEADER_HEIGHT_DESKTOP}px]`,
'sticky',
'top-0',
'pt-[env(safe-area-inset-top)]',
'z-30',
'w-full',
'flex-none',
Expand Down
30 changes: 5 additions & 25 deletions packages/gitbook/src/components/Header/HeaderMobileMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,20 @@
'use client';
import { usePathname } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { useEffect } from 'react';

import { tString, useLanguage } from '@/intl/client';

import { useScrollListener } from '../hooks/useScrollListener';
import { Button, type ButtonProps } from '../primitives';

const globalClassName = 'navigation-open';

const SCROLL_DISTANCE = 320;

/**
* Button to show/hide the table of content on mobile.
*/
export function HeaderMobileMenu(props: ButtonProps) {
const language = useLanguage();

const pathname = usePathname();
const hasScrollRef = useRef(false);

const [isOpen, setIsOpen] = useState(false);

const toggleNavigation = () => {
if (!hasScrollRef.current && document.body.classList.contains(globalClassName)) {
document.body.classList.remove(globalClassName);
setIsOpen(false);
} else {
document.body.classList.add(globalClassName);
window.scrollTo(0, 0);
setIsOpen(true);
}
};

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(() => {
Expand All @@ -50,8 +28,10 @@ export function HeaderMobileMenu(props: ButtonProps) {
variant="blank"
size="default"
label={tString(language, 'table_of_contents_button_label')}
onClick={toggleNavigation}
active={isOpen}
onClick={() => {
document.body.classList.toggle(globalClassName);
}}
// Since the button is hidden behind the TOC after toggling, we don't need to keep track of its active state.
{...props}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ function preloadFont(fontData: FontData) {
* It takes care of setting the theme and the language.
*/
export async function CustomizationRootLayout(props: {
/** The class name to apply to the html element. */
htmlClassName?: string;
/** The class name to apply to the body element. */
className?: string;
bodyClassName?: string;
forcedTheme?: CustomizationThemeMode | null;
context: GitBookAnyContext;
children: React.ReactNode;
}) {
const { className, context, forcedTheme, children } = props;
const { htmlClassName, bodyClassName, context, forcedTheme, children } = props;
const customization =
'customization' in context ? context.customization : defaultCustomization();

Expand Down Expand Up @@ -107,7 +109,8 @@ export async function CustomizationRootLayout(props: {
// Set the dark/light class statically to avoid flashing and make it work when JS is disabled
(forcedTheme ?? customization.themes.default) === CustomizationThemeMode.Dark
? 'dark'
: ''
: '',
htmlClassName
)}
>
<head>
Expand Down Expand Up @@ -179,7 +182,7 @@ export async function CustomizationRootLayout(props: {
}
`}</style>
</head>
<body className={className}>
<body className={tcls(bodyClassName, 'sheet-open:overflow-hidden')}>
<IconsProvider
assetsURL={GITBOOK_ICONS_URL}
assetsURLToken={GITBOOK_ICONS_TOKEN}
Expand Down
1 change: 1 addition & 0 deletions packages/gitbook/src/components/SiteLayout/SiteLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export async function generateSiteLayoutViewport(context: GitBookSiteContext): P
width: 'device-width',
initialScale: 1,
maximumScale: 1,
viewportFit: 'cover',
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`}
>
<div className="flex w-full flex-col px-2">
{sectionsAndGroups.map((item) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,12 @@ 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}
trailingEdgeScrollClassName={children ? 'after:border-tint' : ''}
active={`#${currentSection.id}`}
trailing={{
fade: true,
button: true,
className: children ? 'after:border-tint' : '',
}}
>
<NavigationMenu.List
className={tcls(
Expand Down
Loading