Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
46091b6
Add Structure Preview component and related functionality
conico974 Jun 15, 2026
52fc156
Refactor StructurePreview component and related state management by r…
conico974 Jun 16, 2026
8a5ed60
Refactor StructurePreview and HeaderLogo components to utilize Header…
conico974 Jun 16, 2026
6a87208
Refactor header link components for improved structure and styling co…
conico974 Jun 16, 2026
3e152b0
Refactor layout and page components to streamline structure and enhan…
conico974 Jun 16, 2026
107018f
Refactor SpacesDropdown and related components for improved structure…
conico974 Jun 16, 2026
17d092a
Refactor Search components to enhance structure and functionality, in…
conico974 Jun 16, 2026
96602eb
Refactor AIChatButton and StructurePreview components to enhance AI a…
conico974 Jun 16, 2026
27d9ad6
Refactor Header and StructurePreview components to utilize HeaderLayo…
conico974 Jun 16, 2026
3485c01
Refactor StructurePreviewLogoImage to utilize Image component for imp…
conico974 Jun 16, 2026
f157cfc
Add disableAnimations prop to SiteSectionTabs for improved rendering …
conico974 Jun 16, 2026
334a31d
Refactor TableOfContents styles and components for improved class man…
conico974 Jun 16, 2026
205c512
Refactor variant categorization in StructurePreview to utilize catego…
conico974 Jun 16, 2026
cb649c5
Commit to revert, just to demonstrate what we can do directly in GBO
conico974 Jun 16, 2026
d75cdf1
small comment
conico974 Jun 16, 2026
c334ea2
linting
conico974 Jun 16, 2026
6f63c00
Refactor StructurePreview components and state management to reduce c…
conico974 Jun 17, 2026
53ec8b9
Enhance StructurePreview navigation by adding sectionId handling and …
conico974 Jun 17, 2026
a06da70
Refactor StructurePreview to handle partial updates and navigation me…
conico974 Jun 17, 2026
b0553d5
Skeletons and layout
zenoachtig Jun 17, 2026
2ba9083
fix typescript
conico974 Jun 17, 2026
6eb3d3b
Refactor StructurePreview by removing obsolete test files and updatin…
conico974 Jun 17, 2026
63e4968
Enhance StructurePreview message handling by validating event origin …
conico974 Jun 18, 2026
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
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import { StructurePreview } from '@/components/StructurePreview';
import { getStructurePreviewSnapshot } from '../snapshot';

type PageProps = {
params: Promise<RouteLayoutParams>;
};

export default async function Page(props: PageProps) {
const { context } = await getDynamicSiteContext(await props.params);
return <StructurePreview initialSnapshot={getStructurePreviewSnapshot(context)} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type React from 'react';

import { type RouteLayoutParams, getDynamicSiteContext } from '@/app/utils';
import { CustomizationRootLayout } from '@/components/RootLayout';
import { SiteLayoutClientContexts } from '@/components/SiteLayout/SiteLayoutClientContexts';
import { getThemeFromMiddleware } from '@/lib/middleware';

interface SiteDynamicLayoutProps {
params: Promise<RouteLayoutParams>;
}

export default async function RootLayout({
children,
...props
}: React.PropsWithChildren<SiteDynamicLayoutProps>) {
const { context } = await getDynamicSiteContext(await props.params);
const forcedTheme = await getThemeFromMiddleware();
return (
<CustomizationRootLayout
htmlClassName="sheet-open:gutter-stable overflow-hidden site-background"
bodyClassName="site-background"
forcedTheme={forcedTheme}
context={context}
>
<SiteLayoutClientContexts
contextId={context.contextId}
forcedTheme={
forcedTheme ??
(context.customization.themes.toggeable
? undefined
: context.customization.themes.default)
}
defaultTheme={context.customization.themes.default}
themeStorageKey={`gitbook-theme-structure:${context.site.id}`}
externalLinksTarget={context.customization.externalLinks.target}
proxyOrigin={context.site.proxy?.origin}
>
{children}
</SiteLayoutClientContexts>
</CustomizationRootLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import type {
CustomizationContentLink,
CustomizationHeaderItem,
SiteSection,
SiteSectionGroup,
SiteSpace,
} from '@gitbook/api';
import assertNever from 'assert-never';

import type {
ClientSiteSection,
ClientSiteSectionGroup,
ClientSiteSections,
} from '@/components/SiteSections';
import { categorizeVariants } from '@/components/SpaceLayout/categorizeVariants';
import type { StructurePreviewSnapshot } from '@/components/StructurePreview';
import type { PreviewContentLink, PreviewHeaderLink } from '@/components/StructurePreview/types';
import type { GitBookSiteContext, SiteSections } from '@/lib/context';
import { getLocalizedDescription, getLocalizedTitle } from '@/lib/sites';

export function getStructurePreviewSnapshot(context: GitBookSiteContext): StructurePreviewSnapshot {
const variants = categorizeVariants(context);
const sections = context.visibleSections ?? context.sections;

return {
site: {
title: context.site.title,
},
locale: context.locale,
customization: encodePreviewCustomization(context),
siteSpace: encodePreviewSiteSpace(context.siteSpace, context),
variants: {
generic: variants.generic.map((siteSpace) =>
encodePreviewDropdownSpace(siteSpace, context)
),
translations: variants.translations.map((siteSpace) =>
encodePreviewDropdownSpace(siteSpace, context)
),
},
sections: sections ? encodePreviewSiteSections(context, sections) : null,
icons: {
large: {
light: context.linker.toPathInSpace('~gitbook/icon?size=large&theme=light'),
dark: context.linker.toPathInSpace('~gitbook/icon?size=large&theme=dark'),
},
},
};
}

function encodePreviewCustomization(
context: GitBookSiteContext
): StructurePreviewSnapshot['customization'] {
const { customization, locale } = context;

return {
styling: {
search: customization.styling.search,
},
favicon:
'emoji' in customization.favicon && customization.favicon.emoji
? { emoji: customization.favicon.emoji }
: {},
header: {
preset: customization.header.preset,
logo: customization.header.logo
? {
light: customization.header.logo.light,
dark: customization.header.logo.dark,
}
: undefined,
links: customization.header.links.map((link) => encodePreviewHeaderLink(link, locale)),
},
ai: {
mode: customization.ai.mode,
},
trademark: {
enabled: customization.trademark.enabled,
},
socialAccounts: customization.socialAccounts
.filter((account) => account.display.header === true)
.map((account) => ({
platform: account.platform,
handle: account.handle,
})),
};
}

function encodePreviewHeaderLink(
link: CustomizationHeaderItem,
locale: GitBookSiteContext['locale']
): PreviewHeaderLink {
return {
title: getLocalizedTitle(link, locale),
style: link.style,
hasTarget: Boolean(link.to),
links: link.links.map((subLink) => encodePreviewContentLink(subLink, locale)),
};
}

function encodePreviewContentLink(
link: CustomizationContentLink,
locale: GitBookSiteContext['locale']
): PreviewContentLink {
return {
title: getLocalizedTitle(link, locale),
hasTarget: Boolean(link.to),
};
}

function encodePreviewSiteSpace(
siteSpace: SiteSpace,
context: GitBookSiteContext
): StructurePreviewSnapshot['siteSpace'] {
return {
id: siteSpace.id,
title: getLocalizedTitle(siteSpace, context.locale),
path: siteSpace.path,
};
}

function encodePreviewDropdownSpace(
siteSpace: SiteSpace,
context: GitBookSiteContext
): StructurePreviewSnapshot['variants']['generic'][number] {
return {
id: siteSpace.id,
title: getLocalizedTitle(siteSpace, context.locale),
isActive: siteSpace.id === context.siteSpace.id,
};
}

export function encodePreviewSiteSections(
context: Pick<GitBookSiteContext, 'locale'>,
sections: SiteSections
): ClientSiteSections {
return {
list: sections.list.flatMap((item) => encodePreviewSectionItem(context, item)),
current: encodePreviewSection(context, sections.current),
};
}

function encodePreviewSectionItem(
context: Pick<GitBookSiteContext, 'locale'>,
item: SiteSection | SiteSectionGroup
): (ClientSiteSection | ClientSiteSectionGroup)[] {
switch (item.object) {
case 'site-section':
return [encodePreviewSection(context, item)];
case 'site-section-group': {
const children = item.children.flatMap((child) =>
encodePreviewSectionItem(context, child)
);
if (children.length === 0) {
return [];
}

return [
{
id: item.id,
title: getLocalizedTitle(item, context.locale),
icon: item.icon,
object: item.object,
children,
},
];
}
default:
assertNever(item);
}
}

function encodePreviewSection(
context: Pick<GitBookSiteContext, 'locale'>,
section: SiteSection
): ClientSiteSection {
return {
id: section.id,
title: getLocalizedTitle(section, context.locale),
description: getLocalizedDescription(section, context.locale),
icon: section.icon,
object: section.object,
url: '#',
};
}
45 changes: 37 additions & 8 deletions packages/gitbook/src/components/AIChat/AIChatButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
'use client';
import type { ReactNode } from 'react';

import { useLanguage } from '@/intl/client';
import { t, tString } from '@/intl/translate';
import { tcls } from '@/lib/tailwind';
import type { Assistant } from '../AI';
import { useIsMobile } from '../hooks/useIsMobile';
import { Button } from '../primitives';
Expand All @@ -9,27 +12,30 @@ import { KeyboardShortcut } from '../primitives/KeyboardShortcut';
const MOBILE_BREAKPOINT = 688; // 43rem, equal to Tailwind's @max-2xl container breakpoint

/**
* Button to open/close the AI chat.
* Button visual for an AI assistant in the header.
*/
export function AIChatButton(props: {
assistant: Assistant;
export function AIChatButtonView(props: {
icon: ReactNode;
label: string;
onClick?: () => void;
showLabel?: boolean;
withShortcut?: boolean;
inert?: boolean;
}) {
const { assistant, showLabel = true, withShortcut = true } = props;
const { icon, label, onClick, showLabel = true, withShortcut = true, inert = false } = props;
const language = useLanguage();
const isMobile = useIsMobile(MOBILE_BREAKPOINT, '[data-gb-header-content]');

return (
<Button
icon={assistant.icon}
icon={icon}
data-testid="ai-chat-button"
iconOnly={!showLabel || isMobile}
size="medium"
variant="header"
label={
<div className="flex items-center gap-2">
{t(language, 'ai_chat_ask', assistant.label)}
{t(language, 'ai_chat_ask', label)}
{withShortcut ? (
<KeyboardShortcut
keys={['mod', 'i']}
Expand All @@ -38,10 +44,33 @@ export function AIChatButton(props: {
) : null}
</div>
}
aria-label={tString(language, 'ai_chat_ask', assistant.label)}
onClick={() => assistant.open()}
aria-label={tString(language, 'ai_chat_ask', label)}
onClick={inert ? undefined : onClick}
tabIndex={inert ? -1 : undefined}
className={tcls(inert ? 'pointer-events-none select-none' : null)}
>
{showLabel ? t(language, 'ask') : null}
</Button>
);
}

/**
* Button to open/close the AI chat.
*/
export function AIChatButton(props: {
assistant: Assistant;
showLabel?: boolean;
withShortcut?: boolean;
}) {
const { assistant, showLabel = true, withShortcut = true } = props;

return (
<AIChatButtonView
icon={assistant.icon}
label={assistant.label}
onClick={() => assistant.open()}
showLabel={showLabel}
withShortcut={withShortcut}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { PageBody } from '../PageBody';
import { SiteSectionTabs, encodeClientSiteSections } from '../SiteSections';
import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
import { TableOfContents } from '../TableOfContents';
import {
TABLE_OF_CONTENTS_SPACES_DROPDOWN_CLASS,
getTableOfContentsInnerHeaderClassName,
} from '../TableOfContents/styles';
import { ScrollContainer } from '../primitives/ScrollContainer';
import { EmbeddableDocsPageControlButtons } from './EmbeddableDocsPageControlButtons';
import {
Expand Down Expand Up @@ -129,13 +133,13 @@ export async function EmbeddableDocsPage(
}
innerHeader={
variants.generic.length > 1 ? (
<div className="my-5 sidebar-default:mt-2 flex flex-col gap-2 px-5 empty:hidden">
<div className={getTableOfContentsInnerHeaderClassName()}>
{variants.generic.length > 1 ? (
<SpacesDropdown
context={context}
siteSpace={context.siteSpace}
siteSpaces={variants.generic}
className="w-full px-3"
className={TABLE_OF_CONTENTS_SPACES_DROPDOWN_CLASS}
/>
) : null}
</div>
Expand Down
Loading
Loading