mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-14 06:35:17 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ddcb22e283 | |||
| 0beed6bbf8 | |||
| 2d7954f478 |
@@ -84,6 +84,11 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
ancestorBlocks: props.ancestorBlocks,
|
||||
});
|
||||
|
||||
// Determine if this block should expand to full width or use readable width
|
||||
// Text blocks (paragraphs, headings, lists) use a readable max-width
|
||||
// Visual blocks (code, tables, images, etc.) expand to full width
|
||||
const isFullWidthBlock = FULL_WIDTH_BLOCKS.includes(node.type);
|
||||
|
||||
return (
|
||||
<Block
|
||||
key={node.key || `${node.type}-${index}`}
|
||||
@@ -94,7 +99,12 @@ export function UnwrappedBlocks<TBlock extends DocumentBlock>(props: UnwrappedBl
|
||||
? 'max-w-screen-xl'
|
||||
: 'max-w-3xl',
|
||||
!LIST_BLOCKS.includes(node.type) && 'print:break-inside-avoid',
|
||||
FULL_WIDTH_BLOCKS.includes(node.type) && 'page-width-wide:max-w-full',
|
||||
isFullWidthBlock && 'page-width-wide:max-w-full',
|
||||
// In OpenAPI mode, all blocks expand to full width
|
||||
// In full-width mode, content is capped at 64rem (1024px) and centered
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-full-width:max-w-5xl',
|
||||
'layout-full-width:mx-auto',
|
||||
blockStyle,
|
||||
]}
|
||||
isEstimatedOffscreen={isOffscreen}
|
||||
|
||||
@@ -71,6 +71,8 @@ export function DocumentView(
|
||||
style,
|
||||
// Preserve adjacent whitespace and new lines.
|
||||
'whitespace-pre-wrap',
|
||||
// In OpenAPI mode, add left padding for sidebar offset
|
||||
'layout-openapi:pl-12',
|
||||
]}
|
||||
context={context}
|
||||
isOffscreen={isOffscreen}
|
||||
@@ -85,7 +87,7 @@ export function DocumentViewSkeleton(props: { document: JSONDocument; blockStyle
|
||||
const { document, blockStyle } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 layout-openapi:pl-12">
|
||||
{document.nodes.map((block) => (
|
||||
<BlockSkeleton
|
||||
key={block.key!}
|
||||
@@ -95,6 +97,10 @@ export function DocumentViewSkeleton(props: { document: JSONDocument; blockStyle
|
||||
block.data && 'fullWidth' in block.data && block.data.fullWidth
|
||||
? 'max-w-screen-xl'
|
||||
: 'max-w-3xl',
|
||||
// Expand in OpenAPI mode, cap at 64rem in full-width mode
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-full-width:max-w-5xl',
|
||||
'layout-full-width:mx-auto',
|
||||
blockStyle,
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,16 @@ export function StepperStep(props: BlockProps<DocumentBlockStepperStep>) {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className={tcls('mx-auto flex w-full max-w-3xl flex-row gap-4 md:gap-8', style)}>
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto flex w-full max-w-3xl flex-row gap-4 md:gap-8',
|
||||
// Expand in OpenAPI mode, cap at 64rem in full-width mode
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-full-width:max-w-5xl',
|
||||
'layout-full-width:mx-auto',
|
||||
style
|
||||
)}
|
||||
>
|
||||
<div className="relative select-none">
|
||||
<div
|
||||
className={tcls(
|
||||
|
||||
@@ -224,7 +224,7 @@ const TabPanel = memo(function TabPanel(props: {
|
||||
aria-labelledby={getTabButtonId(tab.id)}
|
||||
className="scroll-mt-[calc(var(--content-scroll-margin)+var(--spacing)*20)]"
|
||||
>
|
||||
<div className="p-4" hidden={!isActive}>
|
||||
<div className={tcls('p-4')} hidden={!isActive}>
|
||||
{tab.body}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,16 +48,12 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
>
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto flex @xs:grid @4xl:max-w-none! max-w-3xl site-width-wide:max-w-screen-2xl flex-col justify-between gap-12',
|
||||
'mx-auto flex @xs:grid @4xl:max-w-none! layout-full-width:max-w-screen-2xl max-w-3xl flex-col justify-between gap-12',
|
||||
'grid-cols-[auto_auto]',
|
||||
'@4xl:grid-cols-[18rem_minmax(auto,48rem)_auto]',
|
||||
'@7xl:grid-cols-[18rem_minmax(auto,48rem)_14rem]',
|
||||
'@4xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_auto]',
|
||||
'@7xl:site-width-wide:grid-cols-[18rem_minmax(auto,80rem)_14rem]',
|
||||
'@4xl:page-no-toc:grid-cols-[minmax(auto,48rem)_auto]',
|
||||
'@7xl:page-no-toc:grid-cols-[14rem_minmax(auto,48rem)_14rem]',
|
||||
'@4xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[minmax(auto,90rem)_auto]',
|
||||
'@7xl:[body:has(.site-width-wide,.page-no-toc)_&]:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
|
||||
'@4xl:layout-full-width:grid-cols-[minmax(auto,90rem)_auto]',
|
||||
'@7xl:layout-full-width:grid-cols-[14rem_minmax(auto,90rem)_14rem]'
|
||||
)}
|
||||
>
|
||||
{
|
||||
@@ -115,10 +111,22 @@ export function Footer(props: { context: GitBookSiteContext }) {
|
||||
customization.footer.groups?.length > 0 ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'@4xl:page-has-toc:col-span-1 @7xl:page-no-toc:col-span-1 col-span-2 @4xl:page-has-toc:col-start-2 @7xl:page-no-toc:col-start-2'
|
||||
'col-span-2',
|
||||
'@4xl:layout-default:col-span-1',
|
||||
'@4xl:layout-default:col-start-2',
|
||||
'@4xl:layout-openapi:col-span-1',
|
||||
'@4xl:layout-openapi:col-start-2',
|
||||
'@7xl:layout-full-width:col-span-1',
|
||||
'@7xl:layout-full-width:col-start-2'
|
||||
)}
|
||||
>
|
||||
<div className="mx-auto flex max-w-3xl site-width-wide:max-w-screen-2xl @xl:flex-row flex-col @xl:gap-6 gap-10">
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto flex max-w-3xl flex-col gap-10',
|
||||
'layout-full-width:max-w-screen-2xl',
|
||||
'@xl:flex-row @xl:gap-6'
|
||||
)}
|
||||
>
|
||||
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
|
||||
(column, columnIndex) => (
|
||||
<div
|
||||
|
||||
@@ -105,7 +105,7 @@ export function Header(props: {
|
||||
'hover:theme-bold:bg-header-link/3',
|
||||
variants.generic.length > 1
|
||||
? 'lg:hidden'
|
||||
: 'page-no-toc:hidden lg:hidden'
|
||||
: 'layout-full-width:hidden lg:hidden'
|
||||
)}
|
||||
/>
|
||||
<HeaderLogo context={context} />
|
||||
|
||||
@@ -36,20 +36,39 @@ export function PageAside(props: {
|
||||
className={tcls(
|
||||
'group/aside',
|
||||
'order-last',
|
||||
'hidden',
|
||||
'max-w-0',
|
||||
'pt-8',
|
||||
'pb-4',
|
||||
'opacity-0',
|
||||
|
||||
'xl:flex',
|
||||
// Hide by default
|
||||
'hidden',
|
||||
|
||||
// Show on xl screens for default layout (normal sidebar)
|
||||
'xl:layout-default:flex',
|
||||
'xl:layout-default:max-w-56',
|
||||
'xl:layout-default:opacity-11',
|
||||
'xl:layout-default:ml-12',
|
||||
|
||||
// Show on xl screens for OpenAPI layout (floating overlay)
|
||||
'xl:layout-openapi:flex',
|
||||
'xl:layout-openapi:opacity-100',
|
||||
'xl:layout-openapi:z-10',
|
||||
'xl:layout-openapi:fixed',
|
||||
'xl:layout-openapi:right-8',
|
||||
'xl:layout-openapi:w-60',
|
||||
'xl:layout-openapi:max-w-60',
|
||||
'xl:layout-openapi:pb-8',
|
||||
'xl:layout-openapi:pt-10',
|
||||
'xl:layout-openapi:ml-0',
|
||||
|
||||
// Always hide for full-width layout
|
||||
'layout-full-width:hidden!',
|
||||
|
||||
'overflow-hidden',
|
||||
|
||||
'xl:max-w-56',
|
||||
'xl:opacity-11',
|
||||
'xl:ml-12',
|
||||
'max-w-0',
|
||||
'opacity-0',
|
||||
|
||||
// Hide when chat is open
|
||||
'xl:max-3xl:chat-open:hidden',
|
||||
'xl:max-3xl:chat-open:max-w-0',
|
||||
'xl:max-3xl:chat-open:opacity-0',
|
||||
@@ -85,17 +104,7 @@ export function PageAside(props: {
|
||||
|
||||
// Client-side dynamic positioning (CSS vars applied by script)
|
||||
'lg:[html[style*="--outline-top-offset"]_&]:top-(--outline-top-offset)!',
|
||||
'lg:[html[style*="--outline-height"]_&]:max-h-(--outline-height)!',
|
||||
|
||||
// When in api page mode, we display it as an overlay on non-large resolutions
|
||||
'xl:max-2xl:page-api-block:z-10',
|
||||
'xl:max-2xl:page-api-block:fixed',
|
||||
'xl:max-2xl:page-api-block:right-8',
|
||||
'xl:max-2xl:page-api-block:w-60',
|
||||
'xl:max-2xl:page-api-block:max-w-60',
|
||||
'xl:max-2xl:page-api-block:pb-8',
|
||||
'xl:max-2xl:page-api-block:pt-10',
|
||||
'xl:max-2xl:[body:has(.openapi-block):has(.page-has-ancestors)_&]:pt-6.5'
|
||||
'lg:[html[style*="--outline-height"]_&]:max-h-(--outline-height)!'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
@@ -104,17 +113,19 @@ export function PageAside(props: {
|
||||
'min-w-56 shrink-0',
|
||||
'overflow-hidden',
|
||||
'w-full',
|
||||
'xl:max-2xl:rounded-corners:page-api-block:rounded-md',
|
||||
'xl:max-2xl:circular-corners:page-api-block:rounded-xl',
|
||||
'xl:max-2xl:page-api-block:border',
|
||||
'xl:max-2xl:page-api-block:border-tint',
|
||||
'xl:max-2xl:page-api-block:bg-tint/9',
|
||||
'xl:max-2xl:page-api-block:backdrop-blur-lg',
|
||||
'xl:max-2xl:contrast-more:page-api-block:bg-tint',
|
||||
'xl:max-2xl:page-api-block:hover:shadow-lg',
|
||||
'xl:max-2xl:page-api-block:hover:shadow-tint-12/1',
|
||||
'xl:max-2xl:dark:page-api-block:hover:shadow-tint-1/1',
|
||||
'xl:max-2xl:page-api-block:not-hover:*:hidden'
|
||||
|
||||
// OpenAPI layout: floating card styles
|
||||
'xl:layout-openapi:rounded-md',
|
||||
'xl:layout-openapi:circular-corners:rounded-xl',
|
||||
'xl:layout-openapi:border',
|
||||
'xl:layout-openapi:border-tint',
|
||||
'xl:layout-openapi:bg-tint/9',
|
||||
'xl:layout-openapi:backdrop-blur-lg',
|
||||
'xl:layout-openapi:contrast-more:bg-tint',
|
||||
'xl:layout-openapi:hover:shadow-lg',
|
||||
'xl:layout-openapi:hover:shadow-tint-12/1',
|
||||
'xl:dark:layout-openapi:hover:shadow-tint-1/1',
|
||||
'xl:layout-openapi:not-hover:*:hidden'
|
||||
)}
|
||||
>
|
||||
<PageAsideHeader context={context} />
|
||||
@@ -136,6 +147,10 @@ export function PageAside(props: {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Header for the aside that shows "ON THIS PAGE" label.
|
||||
* Only visible in OpenAPI layout (floating overlay mode).
|
||||
*/
|
||||
function PageAsideHeader(props: { context: GitBookSiteContext }) {
|
||||
const { context } = props;
|
||||
const language = getSpaceLanguage(context);
|
||||
@@ -144,7 +159,7 @@ function PageAsideHeader(props: { context: GitBookSiteContext }) {
|
||||
<div
|
||||
className={tcls(
|
||||
'hidden',
|
||||
'xl:max-2xl:page-api-block:flex!',
|
||||
'xl:layout-openapi:flex!',
|
||||
'text-xs',
|
||||
'tracking-wide',
|
||||
'font-semibold',
|
||||
@@ -187,7 +202,8 @@ function PageAsideActions(props: {
|
||||
className={tcls(
|
||||
'flex flex-col gap-3',
|
||||
'border-tint-subtle border-t first:border-none',
|
||||
'sidebar-list-default:px-3 pt-5 first:pt-0 xl:max-2xl:page-api-block:p-5',
|
||||
'sidebar-list-default:px-3 pt-5 first:pt-0',
|
||||
'xl:layout-openapi:p-5',
|
||||
'empty:hidden'
|
||||
)}
|
||||
>
|
||||
@@ -209,7 +225,7 @@ async function PageAsideFooter(props: { context: GitBookSiteContext }) {
|
||||
className={tcls(
|
||||
'sticky bottom-0 z-10 mt-auto flex flex-col',
|
||||
'bg-tint-base theme-gradient-tint:bg-gradient-tint theme-gradient:bg-gradient-primary theme-muted:bg-tint-subtle [html.sidebar-filled.theme-bold.tint_&]:bg-tint-subtle',
|
||||
'border-tint-subtle xl:max-2xl:page-api-block:border-t xl:max-2xl:page-api-block:p-2',
|
||||
'border-tint-subtle xl:layout-openapi:border-t xl:layout-openapi:p-2',
|
||||
'pt-4'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,13 @@ import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } f
|
||||
|
||||
import { getSpaceLanguage } from '@/intl/server';
|
||||
import { t } from '@/intl/translate';
|
||||
import { hasFullWidthBlock, hasMoreThan, hasTopLevelBlock, isNodeEmpty } from '@/lib/document';
|
||||
import {
|
||||
hasFullWidthBlock,
|
||||
hasMoreThan,
|
||||
hasOpenAPIBlock,
|
||||
hasTopLevelBlock,
|
||||
isNodeEmpty,
|
||||
} from '@/lib/document';
|
||||
import type { AncestorRevisionPage } from '@/lib/pages';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
@@ -41,6 +47,7 @@ export function PageBody(props: {
|
||||
const { customization } = context;
|
||||
|
||||
const contentFullWidth = document ? hasFullWidthBlock(document) : false;
|
||||
const contentHasOpenAPI = document ? hasOpenAPIBlock(document) : false;
|
||||
|
||||
// Update blocks can only be at the top level of the document, so we optimize the check.
|
||||
const contentHasUpdates = document
|
||||
@@ -55,8 +62,7 @@ export function PageBody(props: {
|
||||
LINK_PREVIEW_MAX_COUNT
|
||||
)
|
||||
: false;
|
||||
const pageWidthWide = page.layout.width === 'wide';
|
||||
const siteWidthWide = pageWidthWide || contentFullWidth;
|
||||
|
||||
const language = getSpaceLanguage(context);
|
||||
const updatedAt = page.updatedAt ?? page.createdAt;
|
||||
|
||||
@@ -65,7 +71,20 @@ export function PageBody(props: {
|
||||
(page) => page.type !== 'document' || (page.type === 'document' && !page.hidden)
|
||||
).length > 0;
|
||||
|
||||
const pageHasToc = page.layout.tableOfContents && hasVisibleTOCItems;
|
||||
const hasTOC = page.layout.tableOfContents && hasVisibleTOCItems;
|
||||
|
||||
// Determine layout mode:
|
||||
// 1. Full-width: No TOC
|
||||
// 2. OpenAPI: Has TOC + (OpenAPI block OR wide property)
|
||||
// 3. Default: Has TOC, no OpenAPI blocks, not wide
|
||||
const layoutMode = !hasTOC
|
||||
? 'layout-full-width'
|
||||
: contentHasOpenAPI || page.layout.width === 'wide'
|
||||
? 'layout-openapi'
|
||||
: 'layout-default';
|
||||
|
||||
// Site-wide width only applies to full-width mode
|
||||
const siteWidthWide = !hasTOC && (page.layout.width === 'wide' || contentFullWidth);
|
||||
|
||||
return (
|
||||
<CurrentPageProvider page={{ spaceId: context.space.id, pageId: page.id }}>
|
||||
@@ -73,15 +92,28 @@ export function PageBody(props: {
|
||||
className={tcls(
|
||||
'relative min-w-0 flex-1',
|
||||
'max-w-screen-2xl py-8',
|
||||
// In full-width layout, expand main to allow cover to go full width
|
||||
'layout-full-width:max-w-full',
|
||||
'layout-full-width:px-0',
|
||||
// Allow words to break if they are too long.
|
||||
'break-anywhere',
|
||||
'@container',
|
||||
pageWidthWide ? 'page-width-wide 3xl:px-8' : 'page-width-default',
|
||||
// Layout mode class for CSS variants
|
||||
layoutMode,
|
||||
// Keep existing classes for backward compatibility
|
||||
hasTOC ? 'page-has-toc' : 'page-no-toc',
|
||||
siteWidthWide ? 'site-width-wide' : 'site-width-default',
|
||||
pageHasToc ? 'page-has-toc' : 'page-no-toc'
|
||||
// Only apply page-width-wide in full-width mode
|
||||
!hasTOC && page.layout.width === 'wide'
|
||||
? 'page-width-wide 3xl:px-8'
|
||||
: 'page-width-default'
|
||||
)}
|
||||
>
|
||||
<PreservePageLayout siteWidthWide={siteWidthWide} pageHasToc={pageHasToc} />
|
||||
<PreservePageLayout
|
||||
siteWidthWide={siteWidthWide}
|
||||
layoutMode={layoutMode}
|
||||
hasTOC={hasTOC}
|
||||
/>
|
||||
{page.cover && page.layout.cover && page.layout.coverSize === 'hero' ? (
|
||||
<PageCover as="hero" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
@@ -128,7 +160,24 @@ export function PageBody(props: {
|
||||
{
|
||||
// TODO: after 25/07/2025, we can chage it to a true check as the cache will be updated
|
||||
page.layout.metadata !== false ? (
|
||||
<div className="mx-auto mt-6 page-api-block:ml-0 flex max-w-3xl page-full-width:max-w-screen-2xl flex-row flex-wrap items-center gap-4 text-tint contrast-more:text-tint-strong">
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto',
|
||||
'mt-6',
|
||||
'flex',
|
||||
'max-w-3xl',
|
||||
'flex-row',
|
||||
'flex-wrap',
|
||||
'items-center',
|
||||
'gap-4',
|
||||
'text-tint',
|
||||
'contrast-more:text-tint-strong',
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-openapi:pl-12',
|
||||
'layout-full-width:max-w-5xl',
|
||||
'layout-full-width:mx-auto'
|
||||
)}
|
||||
>
|
||||
{updatedAt ? (
|
||||
<p className="mr-auto text-sm ">
|
||||
{t(
|
||||
|
||||
@@ -79,6 +79,9 @@ export async function PageBodyBlankslate(props: {
|
||||
'grid',
|
||||
'max-w-3xl',
|
||||
'page-width-wide:max-w-screen-2xl',
|
||||
// Expand to full width in OpenAPI and full-width layout modes
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-full-width:max-w-full',
|
||||
'w-full',
|
||||
'mx-auto',
|
||||
'gap-4',
|
||||
|
||||
@@ -83,22 +83,18 @@ export async function PageCover(props: {
|
||||
data-full={String(as === 'full')}
|
||||
className={tcls(
|
||||
'overflow-hidden',
|
||||
// Negative margin to balance the container padding
|
||||
'-mx-4',
|
||||
'flex',
|
||||
'w-full',
|
||||
'transition-all',
|
||||
as === 'full'
|
||||
? [
|
||||
'sm:-mx-6',
|
||||
'md:-mx-8',
|
||||
!page.layout.tableOfContents &&
|
||||
context.customization.header.preset !== 'none'
|
||||
? [
|
||||
'xl:-ml-76',
|
||||
// Round the bottom corners once the page is wider than the image
|
||||
'2xl:circular-corners:rounded-b-3xl 2xl:rounded-corners:rounded-b-xl',
|
||||
]
|
||||
: [
|
||||
'lg:-ml-12',
|
||||
// Round the bottom left corner once the sidebar is shown next to it
|
||||
'lg:rounded-corners:rounded-bl-xl',
|
||||
'lg:circular-corners:rounded-bl-3xl',
|
||||
@@ -111,6 +107,11 @@ export async function PageCover(props: {
|
||||
'sm:mx-auto',
|
||||
'max-w-3xl ',
|
||||
'page-width-wide:max-w-screen-2xl',
|
||||
// In full-width layout, cover spans full viewport width
|
||||
// Reset negative margins since main has no padding
|
||||
'layout-full-width:mx-0',
|
||||
'layout-full-width:max-w-full',
|
||||
'layout-full-width:rounded-none',
|
||||
'sm:rounded-corners:rounded-xl',
|
||||
'sm:circular-corners:rounded-3xl',
|
||||
'mb-8',
|
||||
|
||||
@@ -35,6 +35,11 @@ export async function PageFooterNavigation(props: {
|
||||
'gap-2',
|
||||
'max-w-3xl',
|
||||
'page-width-wide:max-w-screen-2xl',
|
||||
// Expand in OpenAPI mode, cap at 64rem in full-width mode
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-openapi:pl-12',
|
||||
'layout-full-width:max-w-5xl',
|
||||
'layout-full-width:mx-auto',
|
||||
'mx-auto',
|
||||
'text-tint'
|
||||
)}
|
||||
|
||||
@@ -52,11 +52,15 @@ export async function PageHeader(props: {
|
||||
className={tcls(
|
||||
'max-w-3xl',
|
||||
'page-width-wide:max-w-screen-2xl',
|
||||
// Expand in OpenAPI mode, cap at 64rem in full-width mode
|
||||
'layout-openapi:max-w-full',
|
||||
'layout-openapi:pl-12',
|
||||
'layout-full-width:max-w-5xl',
|
||||
'layout-full-width:mx-auto',
|
||||
'mx-auto',
|
||||
'mb-6',
|
||||
'space-y-3',
|
||||
'page-api-block:ml-0',
|
||||
'page-api-block:max-w-full',
|
||||
hasAncestors ? 'page-has-ancestors' : 'page-no-ancestors'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
'use client';
|
||||
import * as React from 'react';
|
||||
|
||||
const LAYOUT_MODE_CLASSES = ['layout-default', 'layout-openapi', 'layout-full-width'] as const;
|
||||
|
||||
/**
|
||||
* This component preserves the layout of the page while loading a new one.
|
||||
* This approach is needed as page layout (full width block) is done using CSS (`body:has(.full-width)`),
|
||||
* This approach is needed as page layout is done using CSS (`body:has(.layout-*)`),
|
||||
* which becomes false while transitioning between the 2 page states:
|
||||
*
|
||||
* 1. Page 1 with full width block: `body:has(.site-width-wide)` is true
|
||||
* 2. Loading skeleton while transitioning to page 2: `body:has(.site-width-wide)` is false
|
||||
* 3. Page 2 with full width block: `body:has(.site-width-wide)` is true
|
||||
* 1. Page 1 with layout mode: `body:has(.layout-default)` is true
|
||||
* 2. Loading skeleton while transitioning to page 2: `body:has(.layout-default)` is false
|
||||
* 3. Page 2 with layout mode: `body:has(.layout-default)` is true
|
||||
*
|
||||
* This component ensures that the layout is preserved while transitioning between the 2 page states (in step 2).
|
||||
* It also preserves the page TOC state (page-has-toc/page-no-toc) to prevent logo sizing issues during navigation.
|
||||
*/
|
||||
export function PreservePageLayout(props: { siteWidthWide: boolean; pageHasToc: boolean }) {
|
||||
const { siteWidthWide, pageHasToc } = props;
|
||||
export function PreservePageLayout(props: {
|
||||
siteWidthWide: boolean;
|
||||
layoutMode: (typeof LAYOUT_MODE_CLASSES)[number];
|
||||
hasTOC: boolean;
|
||||
}) {
|
||||
const { siteWidthWide, layoutMode, hasTOC } = props;
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
// We use the header as it's an element preserved between page transitions
|
||||
@@ -24,20 +30,29 @@ export function PreservePageLayout(props: { siteWidthWide: boolean; pageHasToc:
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all layout mode classes first
|
||||
for (const cls of LAYOUT_MODE_CLASSES) {
|
||||
header.classList.remove(cls);
|
||||
}
|
||||
|
||||
// Add current layout mode
|
||||
header.classList.add(layoutMode);
|
||||
|
||||
if (siteWidthWide) {
|
||||
header.classList.add('site-width-wide');
|
||||
} else {
|
||||
header.classList.remove('site-width-wide');
|
||||
}
|
||||
|
||||
if (pageHasToc) {
|
||||
// Preserve page TOC state to prevent logo sizing issues during navigation
|
||||
if (hasTOC) {
|
||||
header.classList.add('page-has-toc');
|
||||
header.classList.remove('page-no-toc');
|
||||
} else {
|
||||
header.classList.add('page-no-toc');
|
||||
header.classList.remove('page-has-toc');
|
||||
}
|
||||
}, [siteWidthWide, pageHasToc]);
|
||||
}, [siteWidthWide, layoutMode, hasTOC]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,13 @@ export async function SitePage(props: SitePageProps & { staticRoute: boolean })
|
||||
<PageContextProvider pageId={page.id} spaceId={context.space.id} title={page.title}>
|
||||
{/* Using `contents` makes the children of this div according to its parent — which keeps them in a single flex row with the TOC by default.
|
||||
If there's a page cover, we use `flex flex-col` to lay out the PageCover above the PageBody + PageAside instead. */}
|
||||
<div className={withFullPageCover && page.cover ? 'flex grow flex-col' : 'contents'}>
|
||||
<div
|
||||
className={
|
||||
withFullPageCover && page.cover
|
||||
? 'flex grow flex-col justify-start'
|
||||
: 'contents'
|
||||
}
|
||||
>
|
||||
{withFullPageCover && page.cover ? (
|
||||
<PageCover as="full" page={page} cover={page.cover} context={context} />
|
||||
) : null}
|
||||
|
||||
@@ -129,7 +129,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
'lg:flex-row',
|
||||
'lg:justify-center',
|
||||
CONTAINER_STYLE,
|
||||
'site-width-wide:max-w-screen-4xl',
|
||||
'layout-full-width:max-w-screen-4xl',
|
||||
'transition-[max-width] duration-300',
|
||||
|
||||
// Ensure the footer is display below the viewport even if the content is not enough
|
||||
|
||||
@@ -51,6 +51,10 @@ export async function TableOfContents(props: {
|
||||
'max-lg:not-sidebar-filled:border-r',
|
||||
'border-tint-subtle',
|
||||
|
||||
// Hide completely in full-width mode
|
||||
'layout-full-width:hidden!',
|
||||
'lg:layout-full-width:hidden!',
|
||||
|
||||
'lg:flex!',
|
||||
'lg:animate-none!',
|
||||
'lg:sticky',
|
||||
|
||||
@@ -32,6 +32,20 @@ export function hasFullWidthBlock(document: JSONDocument): boolean {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the document contains an OpenAPI block at the top level.
|
||||
*/
|
||||
export function hasOpenAPIBlock(document: JSONDocument): boolean {
|
||||
return hasTopLevelBlock(document, (block) => {
|
||||
return (
|
||||
block.type === 'swagger' ||
|
||||
block.type === 'openapi-operation' ||
|
||||
block.type === 'openapi-schemas' ||
|
||||
block.type === 'openapi-webhook'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a top level block matches a predicate.
|
||||
*/
|
||||
|
||||
@@ -677,6 +677,16 @@ const config: Config = {
|
||||
*/
|
||||
addVariant('page-api-block', 'body:has(.openapi-block) &');
|
||||
|
||||
/**
|
||||
* Layout mode variants for controlling the page structure.
|
||||
* - layout-default: 3-column layout (TOC + Content + Outline)
|
||||
* - layout-openapi: 2-column layout (TOC + Content, no Outline)
|
||||
* - layout-full-width: 1-column layout (Content only, no TOC or Outline)
|
||||
*/
|
||||
addVariant('layout-default', 'body:has(.layout-default) &');
|
||||
addVariant('layout-openapi', 'body:has(.layout-openapi) &');
|
||||
addVariant('layout-full-width', 'body:has(.layout-full-width) &');
|
||||
|
||||
/**
|
||||
* Variant when the page is displayed in print mode.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user