Skip to content
Draft
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
22 changes: 15 additions & 7 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 not-hydrated:hidden w-96 max-w-full pl-8 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
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,7 +79,7 @@ export async function EmbeddableDocsPage(props: EmbeddableDocsPageProps) {
contentClassName="p-4"
fadeEdges={context.sections ? [] : ['leading']}
>
<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
135 changes: 69 additions & 66 deletions packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
'lg:justify-center',
CONTAINER_STYLE,
'site-width-wide:max-w-screen-4xl',
'hydrated:transition-[max-width] duration-300',
'transition-[max-width] duration-300',

// Ensure the footer is display below the viewport even if the content is not enough
withFooter && [
Expand All @@ -141,79 +141,82 @@ export function SpaceLayout(props: SpaceLayoutProps) {
<TableOfContents
context={context}
header={
withTopHeader ? null : (
<div
className={tcls(
'pr-4',
'flex',
withTopHeader ? 'lg:hidden' : '',
'grow-0',
'dark:shadow-light/1',
'text-base/tight',
'items-center'
)}
>
<HeaderLogo context={context} />
{variants.translations.length > 1 ? (
<TranslationsDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === siteSpace.id
) ?? siteSpace
}
siteSpaces={variants.translations}
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
/>
) : null}
</div>
}
// 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.
innerHeader={
!withTopHeader || variants.generic.length > 1 ? (
<div
className={tcls(
'hidden',
'pr-4',
'mt-2',
'lg:flex',
'grow-0',
'dark:shadow-light/1',
'text-base/tight',
'items-center'
'my-4 sidebar-default:mt-2 flex flex-col gap-2 px-5 empty:hidden',
variants.generic.length > 1 ? '' : 'max-lg:hidden'
)}
>
<HeaderLogo context={context} />
{variants.translations.length > 1 ? (
<TranslationsDropdown
{!withTopHeader && (
<div className="flex gap-2 max-lg:hidden">
<SearchContainer
style={CustomizationSearchStyle.Subtle}
withVariants={variants.generic.length > 1}
withSiteVariants={
visibleSections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.length > 1
) ?? false
}
withSections={withSections}
section={visibleSections?.current}
siteSpace={siteSpace}
siteSpaces={visibleSiteSpaces}
viewport="desktop"
/>
</div>
)}
{!withTopHeader && withSections && visibleSections && (
<SiteSectionList
className="hidden lg:block"
sections={encodeClientSiteSections(
context,
visibleSections
)}
/>
)}
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
siteSpace={
variants.translations.find(
(space) => space.id === siteSpace.id
) ?? siteSpace
}
siteSpaces={variants.translations}
className="[&_.button-leading-icon]:block! ml-auto py-2 [&_.button-content]:hidden"
siteSpace={siteSpace}
siteSpaces={variants.generic}
className="w-full px-3"
/>
) : null}
</div>
)
}
// 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.
innerHeader={
<>
{!withTopHeader && (
<div className="flex gap-2">
<SearchContainer
style={CustomizationSearchStyle.Subtle}
withVariants={variants.generic.length > 1}
withSiteVariants={
visibleSections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.length > 1
) ?? false
}
withSections={withSections}
section={visibleSections?.current}
siteSpace={siteSpace}
siteSpaces={visibleSiteSpaces}
className="max-lg:hidden"
viewport="desktop"
/>
</div>
)}
{!withTopHeader && withSections && visibleSections && (
<SiteSectionList
className={tcls('hidden', 'lg:block')}
sections={encodeClientSiteSections(
context,
visibleSections
)}
/>
)}
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
siteSpace={siteSpace}
siteSpaces={variants.generic}
className="w-full px-3 py-2"
/>
) : null}
</>
) : null
}
/>
{children}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export function PageDocumentItem(props: { page: ClientTOCPageDocument }) {
'my-2',
'border-tint-subtle',
'sidebar-list-default:border-l',
'sidebar-list-line:border-l'
'sidebar-list-line:border-l',
'break-anywhere'
)}
/>
) : null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export function PageGroupItem(props: { page: ClientTOCPageGroup; isFirst?: boole
'[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',
isFirst ? '-mt-6' : ''
)}
>
Expand Down
Loading