From b49caafa9cfa9ae96cb5dcf01cd5f468666ffcd1 Mon Sep 17 00:00:00 2001 From: "Nolann B." <100787331+nolannbiron@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:48:48 +0100 Subject: [PATCH 01/10] Support for hidden sections (#3831) --- .../gitbook/src/components/Header/Header.tsx | 26 +++---- .../src/components/SitePage/SitePage.tsx | 6 +- .../SpaceLayout/SpaceLayout.test.ts | 1 + .../components/SpaceLayout/SpaceLayout.tsx | 17 +++-- .../SpaceLayout/categorizeVariants.ts | 2 +- packages/gitbook/src/lib/context.ts | 72 +++++++++++++++++-- 6 files changed, 97 insertions(+), 27 deletions(-) diff --git a/packages/gitbook/src/components/Header/Header.tsx b/packages/gitbook/src/components/Header/Header.tsx index dcd8e8fb7..89d6cce7a 100644 --- a/packages/gitbook/src/components/Header/Header.tsx +++ b/packages/gitbook/src/components/Header/Header.tsx @@ -25,12 +25,12 @@ export function Header(props: { }; }) { const { context, withTopHeader, variants } = props; - const { siteSpace, siteSpaces, sections, customization } = context; + const { siteSpace, visibleSiteSpaces, visibleSections, customization } = context; const withSections = Boolean( - sections && - (sections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group - sections.list.some((s) => s.object === 'site-section-group')) + visibleSections && + (visibleSections.list.length > 1 || // Show section tabs if there are at least 2 sections or at least 1 section group + visibleSections.list.some((s) => s.object === 'site-section-group')) ); return ( @@ -139,20 +139,22 @@ export function Header(props: { style={customization.styling.search} withVariants={variants.generic.length > 1} withSiteVariants={ - sections?.list.some( + visibleSections?.list.some( (s) => s.object === 'site-section' && s.siteSpaces.length > 1 ) ?? false } - withSections={sections ? sections.list.length > 1 : false} + withSections={ + visibleSections ? visibleSections.list.length > 1 : false + } section={ - sections - ? // Client-encode to avoid a serialisation issue that was causing the language selector to disappear - encodeClientSiteSections(context, sections).current + visibleSections + ? // Client-encode to avoid a serialization issue that was causing the language selector to disappear + encodeClientSiteSections(context, visibleSections).current : undefined } siteSpace={siteSpace} - siteSpaces={siteSpaces} + siteSpaces={visibleSiteSpaces} viewport={!withTopHeader ? 'mobile' : undefined} /> @@ -196,9 +198,9 @@ export function Header(props: { - {sections && withSections ? ( + {visibleSections && withSections ? (
- + {variants.translations.length > 1 ? ( 0); + const withSections = Boolean(visibleSections && visibleSections.list.length > 0); const document = await getPageDocument(context, page); diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts index 1b210ac66..a43c06d6a 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.test.ts @@ -14,6 +14,7 @@ function makeContext(current: FakeSiteSpace, all: FakeSiteSpace[]) { // Only the properties used by categorizeVariants are required for these tests siteSpace: current, siteSpaces: all, + visibleSiteSpaces: all, } as unknown as Parameters[0]; } diff --git a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx index 2f2f2870b..36097a3bc 100644 --- a/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx +++ b/packages/gitbook/src/components/SpaceLayout/SpaceLayout.tsx @@ -99,11 +99,11 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) { */ export function SpaceLayout(props: SpaceLayoutProps) { const { context, children } = props; - const { siteSpace, customization, sections, siteSpaces } = context; + const { siteSpace, customization, visibleSections, visibleSiteSpaces } = context; const withTopHeader = customization.header.preset !== CustomizationHeaderPreset.None; - const withSections = Boolean(sections && sections.list.length > 1); + const withSections = Boolean(visibleSections && visibleSections.list.length > 1); const variants = categorizeVariants(context); const withFooter = @@ -181,25 +181,28 @@ export function SpaceLayout(props: SpaceLayoutProps) { style={CustomizationSearchStyle.Subtle} withVariants={variants.generic.length > 1} withSiteVariants={ - sections?.list.some( + visibleSections?.list.some( (s) => s.object === 'site-section' && s.siteSpaces.length > 1 ) ?? false } withSections={withSections} - section={sections?.current} + section={visibleSections?.current} siteSpace={siteSpace} - siteSpaces={siteSpaces} + siteSpaces={visibleSiteSpaces} className="max-lg:hidden" viewport="desktop" />
)} - {!withTopHeader && withSections && sections && ( + {!withTopHeader && withSections && visibleSections && ( )} {variants.generic.length > 1 ? ( diff --git a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts index 94df27104..81064f59f 100644 --- a/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts +++ b/packages/gitbook/src/components/SpaceLayout/categorizeVariants.ts @@ -5,7 +5,7 @@ import type { GitBookSiteContext } from '@/lib/context'; * Categorize the variants of the space into generic and translation variants. */ export function categorizeVariants(context: GitBookSiteContext) { - const { siteSpace, siteSpaces } = context; + const { siteSpace, visibleSiteSpaces: siteSpaces } = context; const currentLanguage = siteSpace.space.language; // Get all languages of the variants. diff --git a/packages/gitbook/src/lib/context.ts b/packages/gitbook/src/lib/context.ts index 320c8e7c8..f619b39a6 100644 --- a/packages/gitbook/src/lib/context.ts +++ b/packages/gitbook/src/lib/context.ts @@ -120,9 +120,15 @@ export type GitBookSiteContext = GitBookSpaceContext & { /** All site spaces in the current section / or entire site */ siteSpaces: SiteSpace[]; + /** Site spaces that are not hidden (visible to visitors). */ + visibleSiteSpaces: SiteSpace[]; + /** Sections of the site. */ sections: null | SiteSections; + /** Sections filtered to visible site spaces only. */ + visibleSections: null | SiteSections; + /** Customizations of the site. */ customization: SiteCustomizationSettings; @@ -261,9 +267,16 @@ export async function fetchSiteContextByIds( const sections = ids.siteSection ? parseSiteSectionsAndGroups(siteStructure, ids.siteSection) : null; + const visibleSections = ids.siteSection + ? parseVisibleSiteSectionsAndGroups(siteStructure, ids.siteSection) + : null; // Parse the current siteSpace and siteSpaces based on the site structure type. - const { siteSpaces, siteSpace }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace } = (() => { + const { + siteSpaces, + siteSpace, + visibleSiteSpaces, + }: { siteSpaces: SiteSpace[]; siteSpace: SiteSpace; visibleSiteSpaces: SiteSpace[] } = (() => { if (siteStructure.type === 'siteSpaces') { const siteSpaces = siteStructure.structure; const siteSpace = siteSpaces.find((siteSpace) => siteSpace.id === ids.siteSpace); @@ -274,7 +287,7 @@ export async function fetchSiteContextByIds( ); } - return { siteSpaces: filterHiddenSiteSpaces(siteSpaces), siteSpace }; + return { siteSpaces, siteSpace, visibleSiteSpaces: filterHiddenSiteSpaces(siteSpaces) }; } if (siteStructure.type === 'sections') { @@ -295,7 +308,11 @@ export async function fetchSiteContextByIds( ); } - return { siteSpaces: filterHiddenSiteSpaces(siteSpaces), siteSpace }; + return { + siteSpaces, + siteSpace, + visibleSiteSpaces: filterHiddenSiteSpaces(siteSpaces), + }; } // @ts-expect-error @@ -327,10 +344,12 @@ export async function fetchSiteContextByIds( organizationId: ids.organization, site, siteSpaces, + visibleSiteSpaces, siteSpace, customization, structure: siteStructure, sections, + visibleSections, scripts, contextId: ids.contextId, isFallback: ids.isFallback, @@ -434,13 +453,58 @@ function filterHiddenSiteSpaces(siteSpaces: SiteSpace[]): SiteSpace[] { } function parseSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) { - const sectionsAndGroups = getSiteStructureSections(structure, { ignoreGroups: false }); + const sectionsAndGroups = getSiteStructureSections(structure); const section = parseCurrentSection(structure, siteSectionId); assert(section, `couldn't find section "${siteSectionId}" in site structure`); return { list: sectionsAndGroups, current: section } satisfies SiteSections; } +function parseVisibleSiteSectionsAndGroups(structure: SiteStructure, siteSectionId: string) { + const { list: sectionsAndGroups, current: section } = parseSiteSectionsAndGroups( + structure, + siteSectionId + ); + const visibleSectionsAndGroups = filterSectionsAndGroupsWithHiddenSiteSpaces(sectionsAndGroups); + const current = section && !sectionHasOnlyHiddenSiteSpaces(section) ? section : null; + assert(current, `couldn't find section "${siteSectionId}" in site structure`); + return { list: visibleSectionsAndGroups, current } satisfies SiteSections; +} + function parseCurrentSection(structure: SiteStructure, siteSectionId: string) { const sections = getSiteStructureSections(structure, { ignoreGroups: true }); return sections.find((section) => section.id === siteSectionId); } + +type SectionOrGroup = SiteSection | SiteSectionGroup; + +/** + * Filter out sections where all site spaces are hidden and groups that become empty after filtering. + */ +function filterSectionsAndGroupsWithHiddenSiteSpaces( + sectionsOrGroups: SectionOrGroup[] +): SectionOrGroup[] { + return sectionsOrGroups + .map((entry) => { + if (entry.object === 'site-section') { + return sectionHasOnlyHiddenSiteSpaces(entry) ? null : entry; + } + + const visibleChildren: SectionOrGroup[] = filterSectionsAndGroupsWithHiddenSiteSpaces( + entry.children + ); + + if (visibleChildren.length === 0) { + return null; + } + + return { + ...entry, + children: visibleChildren, + }; + }) + .filter((entry): entry is SiteSection | SiteSectionGroup => Boolean(entry)); +} + +function sectionHasOnlyHiddenSiteSpaces(section: SiteSection) { + return section.siteSpaces.every((siteSpace) => siteSpace.hidden); +} From 9e1d7b296b8beb3e1e01a1f69b67e6dd86bd7db8 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 2 Dec 2025 10:24:18 +0100 Subject: [PATCH 02/10] Update AIChat context card open delay (#3834) --- packages/gitbook/src/components/AIChat/AIChatInput.tsx | 2 +- packages/gitbook/src/components/primitives/HoverCard.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/src/components/AIChat/AIChatInput.tsx b/packages/gitbook/src/components/AIChat/AIChatInput.tsx index 98f586fcd..edee25d8e 100644 --- a/packages/gitbook/src/components/AIChat/AIChatInput.tsx +++ b/packages/gitbook/src/components/AIChat/AIChatInput.tsx @@ -108,7 +108,7 @@ export function AIChatInput(props: { ) : null}
- +
Date: Tue, 2 Dec 2025 11:37:55 +0100 Subject: [PATCH 03/10] Fix hidden section not found (#3836) --- .changeset/tiny-carrots-rush.md | 5 +++++ packages/gitbook/src/lib/context.ts | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 .changeset/tiny-carrots-rush.md diff --git a/.changeset/tiny-carrots-rush.md b/.changeset/tiny-carrots-rush.md new file mode 100644 index 000000000..faf9bc5b4 --- /dev/null +++ b/.changeset/tiny-carrots-rush.md @@ -0,0 +1,5 @@ +--- +'gitbook': patch +--- + +Fix hidden section not found diff --git a/packages/gitbook/src/lib/context.ts b/packages/gitbook/src/lib/context.ts index f619b39a6..bfb5f0fb8 100644 --- a/packages/gitbook/src/lib/context.ts +++ b/packages/gitbook/src/lib/context.ts @@ -465,9 +465,7 @@ function parseVisibleSiteSectionsAndGroups(structure: SiteStructure, siteSection siteSectionId ); const visibleSectionsAndGroups = filterSectionsAndGroupsWithHiddenSiteSpaces(sectionsAndGroups); - const current = section && !sectionHasOnlyHiddenSiteSpaces(section) ? section : null; - assert(current, `couldn't find section "${siteSectionId}" in site structure`); - return { list: visibleSectionsAndGroups, current } satisfies SiteSections; + return { list: visibleSectionsAndGroups, current: section } satisfies SiteSections; } function parseCurrentSection(structure: SiteStructure, siteSectionId: string) { From 9e062b10d3d1d75d6f7d21e9f990aa07bf480070 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Wed, 3 Dec 2025 10:39:41 +0100 Subject: [PATCH 04/10] Make semantic text colors more vibrant (#3838) --- .../components/DocumentView/utils/colors.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/utils/colors.ts b/packages/gitbook/src/components/DocumentView/utils/colors.ts index 1b922ed46..15d90d158 100644 --- a/packages/gitbook/src/components/DocumentView/utils/colors.ts +++ b/packages/gitbook/src/components/DocumentView/utils/colors.ts @@ -3,17 +3,17 @@ import type { DocumentMarkColor } from '@gitbook/api'; export const textColorToStyle: { [color in DocumentMarkColor['data']['text']]: ClassValue } = { default: [], - blue: ['text-blue-500'], - red: ['text-red-500'], - green: ['text-green-500'], - yellow: ['text-yellow-600'], - purple: ['text-purple-500'], - orange: ['text-orange-500'], - $primary: ['text-primary'], - $info: ['text-info'], - $success: ['text-success'], - $warning: ['text-warning'], - $danger: ['text-danger'], + blue: ['text-blue-500 contrast-more:text-blue-800'], + red: ['text-red-500 contrast-more:text-red-800'], + green: ['text-green-500 contrast-more:text-green-800'], + yellow: ['text-yellow-600 contrast-more:text-yellow-800'], + purple: ['text-purple-500 contrast-more:text-purple-800'], + orange: ['text-orange-500 contrast-more:text-orange-800'], + $primary: ['text-primary-subtle contrast-more:text-primary'], + $info: ['text-info-subtle contrast-more:text-info'], + $success: ['text-success-subtle contrast-more:text-success'], + $warning: ['text-warning-subtle contrast-more:text-warning'], + $danger: ['text-danger-subtle contrast-more:text-danger'], }; export const backgroundColorToStyle: { From 27b9f7817bf2a06433fedc3466a2c1e90ec47617 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Wed, 3 Dec 2025 11:43:02 +0100 Subject: [PATCH 05/10] Remove corner radius of hint block with heading (#3839) --- packages/gitbook/src/components/DocumentView/Hint.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/Hint.tsx b/packages/gitbook/src/components/DocumentView/Hint.tsx index fc65638e2..978dd441d 100644 --- a/packages/gitbook/src/components/DocumentView/Hint.tsx +++ b/packages/gitbook/src/components/DocumentView/Hint.tsx @@ -32,10 +32,9 @@ export function Hint({ className={tcls( 'hint', 'transition-colors', - 'rounded-md', - hasHeading ? 'rounded-l-sm' : null, - 'straight-corners:rounded-none', + 'rounded-corners:rounded-md', 'circular-corners:rounded-xl', + hasHeading ? 'circular-corners:rounded-l-none rounded-corners:rounded-l-none' : '', 'overflow-hidden', hasHeading ? ['border-l-2', hintStyle.containerWithHeader] : hintStyle.container, From d460dc4b39a2fca0631cb7a18a26b6a5cac45445 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Wed, 3 Dec 2025 13:18:44 +0100 Subject: [PATCH 06/10] Update Stepper styling for theme-muted (#3841) --- .../gitbook/src/components/DocumentView/StepperStep.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/gitbook/src/components/DocumentView/StepperStep.tsx b/packages/gitbook/src/components/DocumentView/StepperStep.tsx index 2c16ab923..4ce319125 100644 --- a/packages/gitbook/src/components/DocumentView/StepperStep.tsx +++ b/packages/gitbook/src/components/DocumentView/StepperStep.tsx @@ -36,13 +36,13 @@ export function StepperStep(props: BlockProps) {
{index + 1}
-
+
Date: Wed, 3 Dec 2025 13:20:22 +0100 Subject: [PATCH 07/10] Support new "link title" page option (#3798) --- bun.lock | 5 ++--- package.json | 2 +- .../TableOfContents/encodeClientTableOfContents.ts | 2 +- packages/gitbook/src/lib/references.tsx | 8 ++++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 2f67c7d8c..5b9d520f0 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "gitbook", @@ -346,7 +345,7 @@ "react-dom": "catalog:", }, "catalog": { - "@gitbook/api": "0.151.0", + "@gitbook/api": "0.153.0", "@scalar/api-client-react": "^1.3.46", "@tsconfig/node20": "^20.1.6", "@tsconfig/strictest": "^2.0.6", @@ -727,7 +726,7 @@ "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="], - "@gitbook/api": ["@gitbook/api@0.151.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-5d9+rZ2u6CKKIiVHO1Toyk+7wHtTOXmP0+sVIE3teRkceX4z5FGIIpa4XsFeKC9XeosncvehdshPaOGQtSDpTQ=="], + "@gitbook/api": ["@gitbook/api@0.153.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ArNPFoqwId4Flicz8xPEdGqXQkzxyxP7S8Uv3wIfCX2e4cLhpcS7xCJGEHOJrqe1tNs1ovT1N2MWfpdIqzXqig=="], "@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"], diff --git a/package.json b/package.json index b83bbd2e9..54ec8c2aa 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "catalog": { "@tsconfig/strictest": "^2.0.6", "@tsconfig/node20": "^20.1.6", - "@gitbook/api": "0.151.0", + "@gitbook/api": "0.153.0", "@scalar/api-client-react": "^1.3.46", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", diff --git a/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts b/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts index 3c3820c3f..d087e80dc 100644 --- a/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts +++ b/packages/gitbook/src/components/TableOfContents/encodeClientTableOfContents.ts @@ -72,7 +72,7 @@ export async function encodeClientTableOfContents( result.push( removeUndefined({ id: page.id, - title: page.title, + title: page.linkTitle ?? page.title, href, emoji: page.emoji, icon: page.icon, diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index a0051ead9..c9b6aefa5 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -129,7 +129,7 @@ export async function resolveContentRef( const page = resolvePageResult?.page; const ancestors = resolvePageResult?.ancestors.map((ancestor) => ({ - label: ancestor.title, + label: ancestor.linkTitle ?? ancestor.title, icon: ancestor.emoji || ancestor.icon ? ( , href, }); @@ -177,7 +177,7 @@ export async function resolveContentRef( parentPage && contentRef.page === parentPage.id && parentPage.type === 'group' ? parentPage : page; - text = pageOrGroup.title; + text = pageOrGroup.linkTitle ?? pageOrGroup.title; emoji = isCurrentPage ? undefined : page.emoji; icon = ; } From 3fbd6f0d503d4077eefe1bbb26f2b30d11c6aea3 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Wed, 3 Dec 2025 13:21:38 +0100 Subject: [PATCH 08/10] Hide empty Footer correctly (#3842) --- packages/gitbook/src/components/Footer/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gitbook/src/components/Footer/Footer.tsx b/packages/gitbook/src/components/Footer/Footer.tsx index 27a64c0b8..e909bba3d 100644 --- a/packages/gitbook/src/components/Footer/Footer.tsx +++ b/packages/gitbook/src/components/Footer/Footer.tsx @@ -29,7 +29,7 @@ export function Footer(props: { context: GitBookSiteContext }) { className={tcls( 'border-tint-subtle border-t', // If the footer only contains a mode toggle, we only show it on smaller screens - mobileOnly ? '@7xl:hidden' : null + mobileOnly ? 'xl:hidden' : null )} >
From 1430ceebea974666116b169e06b9bc4e85925494 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Thu, 4 Dec 2025 10:01:25 +0100 Subject: [PATCH 09/10] Update Docs Embed with new styling and tabs (#3823) --- .changeset/grumpy-peas-hammer.md | 6 + packages/embed/README.md | 378 +++++++++++++++++- packages/embed/src/client/createGitBook.ts | 2 +- .../embed/src/client/createGitBookFrame.ts | 5 +- packages/embed/src/client/protocol.ts | 20 +- packages/embed/src/react/GitBookFrame.tsx | 9 +- packages/embed/src/standalone/index.ts | 75 ++-- packages/embed/src/standalone/style.css | 12 + .../~gitbook/embed/assistant/page.tsx | 25 +- .../[siteData]/~gitbook/embed/page.tsx | 21 + .../~gitbook/embed/assistant/page.tsx | 25 +- .../[siteData]/~gitbook/embed/page.tsx | 23 ++ packages/gitbook/src/components/AI/useAI.tsx | 1 + .../gitbook/src/components/AI/useAIChat.tsx | 64 ++- .../gitbook/src/components/AIChat/AIChat.tsx | 54 +-- .../components/AIChat/AIChatControlButton.tsx | 37 +- .../Embeddable/EmbeddableAIChat.tsx | 75 +++- .../Embeddable/EmbeddableAssistantPage.tsx | 9 +- .../Embeddable/EmbeddableDocsPage.tsx | 90 +++-- .../EmbeddableDocsPageControlButtons.tsx | 21 + .../components/Embeddable/EmbeddableFrame.tsx | 31 +- .../Embeddable/EmbeddableIframeAPI.tsx | 117 +++++- .../Embeddable/EmbeddableRootLayout.tsx | 26 +- .../components/Header/HeaderMobileMenu.tsx | 31 +- .../SiteSections/SiteSectionTabs.tsx | 17 +- .../TableOfContents/TableOfContents.tsx | 6 +- .../components/TableOfContents/Trademark.tsx | 36 +- .../src/components/primitives/Button.tsx | 43 +- .../src/components/primitives/Link.tsx | 4 +- .../src/components/primitives/Tooltip.tsx | 6 + packages/gitbook/src/lib/embeddable.ts | 11 + packages/gitbook/src/middleware.ts | 1 + 32 files changed, 1045 insertions(+), 236 deletions(-) create mode 100644 .changeset/grumpy-peas-hammer.md create mode 100644 packages/gitbook/src/app/sites/dynamic/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx create mode 100644 packages/gitbook/src/app/sites/static/[mode]/[siteURL]/[siteData]/~gitbook/embed/page.tsx create mode 100644 packages/gitbook/src/components/Embeddable/EmbeddableDocsPageControlButtons.tsx diff --git a/.changeset/grumpy-peas-hammer.md b/.changeset/grumpy-peas-hammer.md new file mode 100644 index 000000000..abc1f9a18 --- /dev/null +++ b/.changeset/grumpy-peas-hammer.md @@ -0,0 +1,6 @@ +--- +"@gitbook/embed": minor +"gitbook": patch +--- + +Improve Docs Embed with separate Assistant and Docs tabs diff --git a/packages/embed/README.md b/packages/embed/README.md index 45efed3ac..089b603a3 100644 --- a/packages/embed/README.md +++ b/packages/embed/README.md @@ -1,24 +1,61 @@ -# `@gitbook/embed` +# GitBook Docs Embed (`@gitbook/embed`) -Embed the GitBook Docs Assistant in your product or website. +Embed your GitBook docs in your product or website. + +The Docs Embed can contain two tabs: +- **Assistant**: The [GitBook Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) - an AI-powered chat interface to help users find answers +- **Docs**: A browser for navigating your documentation site + +The embed is set up automatically based on your site's configuration. You can optionally customize and override the configuration with custom actions, tools, suggested questions, [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access), and more. See the [Configuration](#configuration) section for all available options. # Usage -## As a script from your docs site +## As a standalone script from your docs site -All GitBook docs site includes a script to easily embed the docs assistant as a widget on your website. +All GitBook docs sites include a script to easily add the Docs Embed as a widget on your website. The script is served at `https://docs.company.com/~gitbook/embed/script.js`. -You can find the embed script from your docs site settings, or you can copy the following and replace the `docs.company.com` by your docs site hostname. +You can find the embed script from your docs site settings, or you can copy the following and replace `docs.company.com` with your docs site hostname. ```html ``` +The standalone script provides a global `GitBook` function. See the [API Reference](#api-reference) section for all available methods. + +### Example: Configuring the widget + +```javascript +GitBook('configure', { + button: { + label: 'Ask', + icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book' + }, + tabs: ['assistant', 'docs'], + actions: [ + { + icon: 'circle-question', + label: 'Contact Support', + onClick: () => window.open('https://support.example.com', '_blank') + } + ], + greeting: { title: 'Welcome!', subtitle: 'How can I help?' }, + suggestions: ['What is GitBook?', 'How do I get started?'], + tools: [/* ... */] +}); +``` + +See the [Configuration](#configuration) section for all available options. + ## As a package from NPM Install the package: `npm install @gitbook/embed` and import it in your web application: @@ -30,10 +67,46 @@ const gitbook = createGitBook({ siteURL: 'https://docs.company.com' }); +// Create an iframe and get its URL const iframe = document.createElement('iframe'); -iframe.src = gitbook.getFrameURL(); +iframe.src = gitbook.getFrameURL({ + visitor: { + token: 'your-jwt-token', // Optional: for Adaptive Content or Authenticated Access + unsignedClaims: { // Optional: custom claims for dynamic expressions + userId: '123', + plan: 'premium' + } + } +}); +// Create a frame client to communicate with the iframe const frame = gitbook.createFrame(iframe); + +// Use the frame client methods +frame.navigateToPage('/getting-started'); // Navigate to a page in the docs tab +frame.navigateToAssistant(); // Switch to the assistant tab +frame.postUserMessage('How do I get started?'); +frame.clearChat(); + +// Configure the embed (see Configuration section for all options) +frame.configure({ + tabs: ['assistant', 'docs'], + actions: [ + { + icon: 'circle-question', + label: 'Contact Support', + onClick: () => window.open('https://support.example.com', '_blank') + } + ], + greeting: { title: 'Welcome!', subtitle: 'How can I help?' }, + suggestions: ['What is GitBook?', 'How do I get started?'], + tools: [/* ... */] +}); + +// Listen to events +frame.on('close', () => { + console.log('Frame closed'); +}); ``` ## As React components @@ -41,9 +114,298 @@ const frame = gitbook.createFrame(iframe); After installing the NPM package, you can import prebuilt React components: ```tsx -import { GitBookProvider, GitBookAssistantFrame } from '@gitbook/embed/react'; +import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react'; - + window.open('https://support.example.com', '_blank') + } + ]} + tools={[/* ... */]} + /> ``` + +You can also use the `useGitBook` hook to access the client: + +```tsx +import { useGitBook } from '@gitbook/embed/react'; + +function MyComponent() { + const gitbook = useGitBook(); + const frameURL = gitbook.getFrameURL({ visitor: { token: '...' } }); + // ... +} +``` + +# API Reference + +## Method Comparison + +| Method | Standalone Script | NPM Package | React Components | +|--------|------------------|-------------|------------------| +| **Initialize** | `GitBook('init', options, frameOptions)` | `createGitBook(options)` | `` | +| **Get frame URL** | ❌ (handled internally) | `client.getFrameURL(options)` | `useGitBook().getFrameURL(options)` | +| **Create frame client** | ❌ (handled internally) | `client.createFrame(iframe)` | `useGitBook().createFrame(iframe)` | +| **Show/Hide widget** | `GitBook('show')` / `GitBook('hide')` | ❌ | ❌ | +| **Open/Close window** | `GitBook('open')` / `GitBook('close')` / `GitBook('toggle')` | ❌ | ❌ | +| **Navigate to page** | `GitBook('navigateToPage', path)` | `frame.navigateToPage(path)` | Via frame client | +| **Navigate to assistant** | `GitBook('navigateToAssistant')` | `frame.navigateToAssistant()` | Via frame client | +| **Post message** | `GitBook('postUserMessage', message)` | `frame.postUserMessage(message)` | Via frame client | +| **Clear chat** | `GitBook('clearChat')` | `frame.clearChat()` | Via frame client | +| **Configure** | `GitBook('configure', settings)` | `frame.configure(settings)` | Props on `` | +| **Event listeners** | ❌ | `frame.on(event, listener)` | Via frame client | +| **Unload** | `GitBook('unload')` | ❌ | ❌ | + +## Method Signatures + +### Standalone Script + +- `GitBook('init', options: { siteURL: string }, frameOptions?: { visitor?: {...} })` - Initialize widget +- `GitBook('show')` - Show widget button +- `GitBook('hide')` - Hide widget button +- `GitBook('open')` - Open widget window +- `GitBook('close')` - Close widget window +- `GitBook('toggle')` - Toggle widget window +- `GitBook('navigateToPage', path: string)` - Navigate to page +- `GitBook('navigateToAssistant')` - Navigate to assistant tab +- `GitBook('postUserMessage', message: string)` - Post message to chat +- `GitBook('clearChat')` - Clear chat history +- `GitBook('configure', settings: {...})` - Configure widget +- `GitBook('unload')` - Unload widget + +### NPM Package + +**Client Factory:** +- `createGitBook(options: { siteURL: string })` → `GitBookClient` +- `client.getFrameURL(options?: { visitor?: {...} })` → `string` +- `client.createFrame(iframe: HTMLIFrameElement)` → `GitBookFrameClient` + +**Frame Client:** +- `frame.navigateToPage(path: string)` → `void` +- `frame.navigateToAssistant()` → `void` +- `frame.postUserMessage(message: string)` → `void` +- `frame.clearChat()` → `void` +- `frame.configure(settings: Partial)` → `void` +- `frame.on(event: string, listener: Function)` → `() => void` (unsubscribe) + +### React Components + +**Components:** +- `` - Provider component +- `` - Frame component (accepts all config options as props) + +**Hooks:** +- `useGitBook()` → `GitBookClient` (must be used within ``) + +# Configuration + +Configuration options are available across usage methods as follows: +- **Standalone script**: via `GitBook('configure', {...})` +- **NPM package**: via `frame.configure({...})` +- **React components**: via props on `` + +### `tabs` + +Available in: Standalone script, NPM package, React components + +Override which tabs are displayed. Defaults to your site's configuration. + +- **Type**: `('assistant' | 'docs')[]` +- **Options**: + - `['assistant', 'docs']` - Show both tabs + - `['assistant']` - Show only the assistant tab + - `['docs']` - Show only the docs tab + +```javascript +tabs: ['assistant', 'docs'] +``` + +### `actions` + +Available in: Standalone script, NPM package, React components + +Custom action buttons rendered in the sidebar alongside tabs. Each action button triggers a callback when clicked. + +**Note**: This prop was previously named `buttons`. Use `actions` instead, it has the same functionality. + +- **Type**: `GitBookEmbeddableActionDefinition[]` +- **Properties**: + - `icon`: `string` - Icon name. Any [FontAwesome icon](https://fontawesome.com/search) is supported. (e.g., `'rocket'`, `'comments'`, `'user-circle'`, ...) + - `label`: `string` - Button label text + - `onClick`: `() => void | Promise` - Callback function when clicked + +```javascript +actions: [ + { + icon: 'comments', + label: 'Contact Support', + onClick: () => window.open('https://support.example.com', '_blank') + }, + { + icon: 'rocket', + label: 'Get started', + onClick: () => { + GitBook('navigateToPage', '/getting-started'); + } + } +] +``` + +### `greeting` + +Available in: Standalone script, NPM package, React components + +Welcome message displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) tab. + +- **Type**: `{ title: string, subtitle: string }` + +```javascript +greeting: { + title: 'Welcome!', + subtitle: 'How can I help you today?' +} +``` + +### `suggestions` + +Available in: Standalone script, NPM package, React components + +Suggested questions displayed in the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant) welcome screen. + +- **Type**: `string[]` + +```javascript +suggestions: [ + 'What is GitBook?', + 'How do I get started?', + 'What can you do?' +] +``` + +### `tools` + +Available in: Standalone script, NPM package, React components + +Custom AI tools to extend the [Assistant](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant). Tools allow the assistant to execute functions and integrate with your own systems. + +**Note**: In addition to custom tools, the Assistant will always have access to any [MCP servers you define](https://gitbook.com/docs/publishing-documentation/gitbook-ai-assistant#extend-gitbook-assistant-with-mcp-servers) in your site's AI settings. + +- **Type**: `GitBookToolDefinition[]` +- **Properties**: + - `name`: `string` - Unique tool identifier + - `description`: `string` - Description of what the tool does (used by the AI to decide when and how to use it). + - `inputSchema`: `object` - JSON schema defining the tool's input parameters + - `execute`: `(input: object) => Promise<{ output: any, summary: string }>` - Async function that executes the tool. + - `output`: The result of the tool execution, provided to the AI to continue working with. Not shown to the user. + - `summary`: The visual summary of the tool execution, shown in the user's chat window. + - `confirmation`: `{ icon?: string, label: string }` (optional) - Confirmation button shown before execution, useful for actions that require the user's express approval. + +```javascript +tools: [ + { + name: 'get_user_info', + description: 'Get information about the current user', + inputSchema: { + type: 'object', + properties: { + userId: { + type: 'string', + description: 'The user ID to look up' + } + }, + required: ['userId'] + }, + execute: async (input) => { + const user = await fetch(`/api/users/${input.userId}`).then(r => r.json()); + return { + output: { name: user.name, plan: user.plan }, + summary: `Retrieved info for user ${user.name}` + }; + } + }, + { + name: 'create_ticket', + description: 'Create a support ticket', + confirmation: { + icon: 'circle-question', + label: 'Create support ticket?' + }, + inputSchema: { + type: 'object', + properties: { + subject: { type: 'string' }, + description: { type: 'string' } + }, + required: ['subject', 'description'] + }, + execute: async (input) => { + const ticket = await fetch('/api/tickets', { + method: 'POST', + body: JSON.stringify(input) + }).then(r => r.json()); + return { + output: { ticketId: ticket.id }, + summary: `Created ticket #${ticket.id}` + }; + } + } +] +``` + +### `visitor` (Authenticated Access) + +Available in: Standalone script (via `init`), NPM package (via `getFrameURL()`), React components (as prop) + +[Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) options passed when creating the frame URL. Used for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) and [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access). + +**Note**: This is not a configuration option but rather a parameter when initializing the frame or creating the frame URL. + +**Standalone script**: Pass as the second argument to `GitBook('init', options, frameOptions)` +**NPM package**: Pass to `getFrameURL({ visitor: {...} })` +**React components**: Pass as the `visitor` prop on `` + +- **Type**: `{ token?: string, unsignedClaims?: Record }` +- **Properties**: + - `token`: `string` (optional) - Signed JWT token for [Adaptive Content](https://gitbook.com/docs/publishing-documentation/adaptive-content) or [Authenticated Access](https://gitbook.com/docs/publishing-documentation/authenticated-access) + - `unsignedClaims`: `Record` (optional) - Unsigned claims that can be used in dynamic expressions via `visitor.claims.unsigned.` + +```javascript +visitor: { + token: 'your-jwt-token', + unsignedClaims: { + userId: '123', + plan: 'premium', + role: 'admin' + } +} +``` + +### `button` + +Available in: Standalone script only + +Configure the widget button for the standalone script. This option is not available when using the NPM package or React components, since they can be customized completely. + +- **Type**: `{ label: string, icon: 'assistant' | 'sparkle' | 'help' | 'book' }` +- **Properties**: + - `label`: `string` - Button label text + - `icon`: `'assistant' | 'sparkle' | 'help' | 'book'` - Icon displayed on the button. Choose from one of 4 presets. + +```javascript +button: { + label: 'Ask', + icon: 'assistant' +} +``` diff --git a/packages/embed/src/client/createGitBook.ts b/packages/embed/src/client/createGitBook.ts index bcd57a096..fc39c71bb 100644 --- a/packages/embed/src/client/createGitBook.ts +++ b/packages/embed/src/client/createGitBook.ts @@ -40,7 +40,7 @@ export function createGitBook(options: CreateGitBookOptions) { const client: GitBookClient = { getFrameURL: (frameOptions) => { const url = new URL(options.siteURL); - url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed/assistant`; + url.pathname = `${url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`}~gitbook/embed`; if (frameOptions.visitor?.token) { url.searchParams.set('token', frameOptions.visitor.token); diff --git a/packages/embed/src/client/createGitBookFrame.ts b/packages/embed/src/client/createGitBookFrame.ts index baaabf647..84908521b 100644 --- a/packages/embed/src/client/createGitBookFrame.ts +++ b/packages/embed/src/client/createGitBookFrame.ts @@ -64,8 +64,9 @@ export function createGitBookFrame(iframe: HTMLIFrameElement): GitBookFrameClien const events = new Map void>>(); const configuration: GitBookEmbeddableConfiguration = { - buttons: [], - welcomeMessage: '', + tabs: ['assistant', 'docs'], + actions: [], + greeting: { title: '', subtitle: '' }, suggestions: [], tools: [], }; diff --git a/packages/embed/src/client/protocol.ts b/packages/embed/src/client/protocol.ts index f19b400e3..ea7882b72 100644 --- a/packages/embed/src/client/protocol.ts +++ b/packages/embed/src/client/protocol.ts @@ -23,7 +23,7 @@ export type GitBookToolDefinition = AIToolDefinition & { /** * Custom button definition to be passed to the embeddable GitBook. */ -export type GitBookEmbeddableButtonDefinition = { +export type GitBookEmbeddableActionDefinition = { /** * Icon to be displayed in the button. */ @@ -41,16 +41,26 @@ export type GitBookEmbeddableButtonDefinition = { }; /** - * Overall configuration for the layout of the embeddable GitBook. + * Overall configuration for the layout of the GitBook embed. */ export type GitBookEmbeddableConfiguration = { + /** Tabs to display in the embed (if enabled on the site). */ + tabs: ('assistant' | 'docs')[]; + + /** Additional buttons to be displayed in the header of the GitBook embed. */ + actions: GitBookEmbeddableActionDefinition[]; + /** - * Buttons to be displayed in the header of the embeddable GitBook. + * Additional buttons to be displayed in the header of the GitBook embed. + * @deprecated Use `actions` instead. */ - buttons: GitBookEmbeddableButtonDefinition[]; + buttons?: GitBookEmbeddableActionDefinition[]; /** Message to be displayed in the welcome page. */ - welcomeMessage: string; + greeting: { + title: string; + subtitle: string; + }; /** Suggestions of questions to be displayed in the welcome page. */ suggestions: string[]; diff --git a/packages/embed/src/react/GitBookFrame.tsx b/packages/embed/src/react/GitBookFrame.tsx index cd0d8f65d..b9f527fea 100644 --- a/packages/embed/src/react/GitBookFrame.tsx +++ b/packages/embed/src/react/GitBookFrame.tsx @@ -17,7 +17,7 @@ export type GitBookFrameProps = { * Render a frame with the GitBook Assistant in it. */ export function GitBookFrame(props: GitBookFrameProps) { - const { className, visitor, buttons, welcomeMessage, suggestions, tools } = props; + const { className, visitor, actions, greeting, suggestions, tools } = props; const frameRef = useRef(null); const gitbook = useGitBook(); @@ -33,12 +33,13 @@ export function GitBookFrame(props: GitBookFrameProps) { useEffect(() => { gitbookFrame?.configure({ - buttons, - welcomeMessage, + tabs: ['assistant', 'docs'], + actions, + greeting, suggestions, tools, }); - }, [gitbookFrame, buttons, welcomeMessage, suggestions, tools]); + }, [gitbookFrame, actions, greeting, suggestions, tools]); return (