Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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/hot-views-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"gitbook": patch
---

Add cover image background mode and masks
1 change: 1 addition & 0 deletions packages/gitbook/src/components/DocumentView/Heading.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function Heading(props: BlockProps<DocumentBlockHeading>) {
'justify-self-start',
'max-w-full',
'break-words',
'page-cover-background:text-contrast-cover',
getTextAlignment(block.data.align),
textStyle.lineHeight
)}
Expand Down
9 changes: 8 additions & 1 deletion packages/gitbook/src/components/DocumentView/Paragraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,14 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
'has-[.button,input]:flex has-[.button,input]:flex-wrap has-[.button,input]:gap-2 has-[.button,input]:items-center';

return (
<p className={tcls(inlineButtonStyle, style, getTextAlignment(block.data?.align))}>
<p
className={tcls(
'page-cover-background:[&:not(:has(.button,input))]:text-contrast-cover',
inlineButtonStyle,
style,
getTextAlignment(block.data?.align)
)}
>
<Inlines {...contextProps} nodes={block.nodes} ancestorInlines={[]} />
</p>
);
Expand Down
6 changes: 4 additions & 2 deletions packages/gitbook/src/components/PageAside/PageAside.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ export function PageAside(props: {
'page-api-block:page-has-outline:min-[96rem]:border-l-0',
'page-api-block:page-has-outline:min-[96rem]:pl-8',

'hydrated:site-background', // Only add a background once the element is positioned correctly to prevent overlapping the page cover
'layout-default:max-xl:site-background',
'layout-wide:max-3xl:site-background',
'text-tint',
'contrast-more:text-tint-strong'
'contrast-more:text-tint-strong',
'xl:page-cover-background:text-contrast-cover'
)}
>
<div className="flex h-full w-full shrink-0 flex-col overflow-hidden">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export function ScrollSectionsList({ sections }: { sections: DocumentSection[] }
'sidebar-list-line:border-l-2',
'border-transparent',
'sidebar-list-line:-left-px',
'xl:page-cover-background:text-contrast-cover',

section.depth > 1 && [
'subitem',
Expand Down
3 changes: 2 additions & 1 deletion packages/gitbook/src/components/PageBody/PageBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function PageBody(props: {
'py-8',
'layout-wide:no-sidebar:lg:max-xl:pb-20', // Add padding to prevent overlap of minimised trademark
'@container',
'flex flex-col',
CONTENT_STYLE,
pageHasToc ? 'page-has-toc' : 'page-no-toc',
wideLayout ? 'layout-wide' : 'layout-default'
Expand All @@ -106,7 +107,7 @@ export function PageBody(props: {
<SuspenseLoadedHint />
<DocumentView
document={document}
style="clear-both flex flex-col [&>*+*]:mt-5"
style="clear-both flex grow flex-col [&>*+*]:mt-5"
context={{
mode: 'default',
contentContext: {
Expand Down
148 changes: 94 additions & 54 deletions packages/gitbook/src/components/PageBody/PageCover.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { GitBookSiteContext } from '@/lib/context';
import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api';
import {
CustomizationHeaderPreset,
type RevisionPageDocument,
type RevisionPageDocumentCover,
} from '@gitbook/api';
import type { StaticImageData } from 'next/image';

import { getImageAttributes } from '@/components/utils';
Expand All @@ -13,19 +17,40 @@ import { getCoverHeight } from './coverHeight';
import defaultPageCoverSVG from './default-page-cover.svg';

const defaultPageCover = defaultPageCoverSVG as StaticImageData;
const DEFAULT_RESPONSIVE_COVER_CUTOFF = '56.25%';

/**
* Cover for the page.
*/
export async function PageCover(props: {
as: 'hero' | 'full';
as: 'hero' | 'full' | 'background';
page: RevisionPageDocument;
cover: RevisionPageDocumentCover;
context: GitBookSiteContext;
}) {
const { as, cover, context } = props;
let { as, cover, context } = props;
const height = getCoverHeight(cover);

const initialCoverCutoff = () => {
if (!height) {
return DEFAULT_RESPONSIVE_COVER_CUTOFF;
}

let total = height;
if (context.customization.announcement?.enabled) {
total += 68;
}
if (context.customization.header.preset !== CustomizationHeaderPreset.None) {
total += 64;
}
if (context.visibleSections && context.visibleSections.list.length > 1) {
total += 45;
}
return `${total}px`;
};

as = 'background';

const [resolved, resolvedDark] = await Promise.all([
cover.ref ? resolveContentRef(cover.ref, context) : null,
cover.refDark ? resolveContentRef(cover.refDark, context) : null,
Expand Down Expand Up @@ -79,56 +104,71 @@ export async function PageCover(props: {
assert(light, 'Light image should be defined');

return (
<div
data-gb-page-cover
data-full={String(as === 'full')}
className={tcls(
'overflow-hidden',
// Negative margin to balance the container padding
'-mx-4',

// Full-width cover: extend to edges, disregard TOC where possible
as === 'full'
? [
'sm:-mx-6',
'md:-mx-8',
'lg:-ml-12',

// Extend the full-width cover
'layout-default:page-no-toc:lg:-ml-92', // Extend into the left sidebar if there's no TOC...
'layout-wide:2xl:-mr-[clamp(2rem,calc((100vw-90rem)/2+2rem),18rem)]', // ...and to the right if there's no outline.
'layout-wide:page-no-toc:2xl:-mx-[max(calc((100vw-90rem)/2+2rem),2rem)]', // Span full width if the page content is centered.
'layout-wide:has-sidebar:page-no-toc:lg:-ml-[max(calc((100vw-90rem)/2+23rem),23rem)]', // If there's still a sidebar, we have to factor it in too.

// Corner rounding: we round once the page is wide enough to have space around the cover.
'layout-default:2xl:rounded-corners:rounded-b-xl',
'layout-default:2xl:circular-corners:rounded-b-3xl',
'layout-wide:3xl:circular-corners:rounded-b-3xl',
'layout-wide:3xl:rounded-corners:rounded-b-xl',
// Round the bottom left corner once the sidebar is shown next to it
'has-sidebar:lg:rounded-corners:rounded-bl-xl',
'has-sidebar:lg:circular-corners:rounded-bl-3xl',
]
: [
// Regular cover: size regularly along with other content
CONTENT_STYLE,
'max-sm:-mx-4',
'sm:rounded-corners:rounded-xl',
'sm:circular-corners:rounded-3xl',
'mb-8',
'max-sm:w-screen',
'max-sm:-mt-8',
]
)}
>
<PageCoverImage
imgs={{
light,
dark,
}}
y={cover.yPos}
height={height}
/>
</div>
<>
<style>{`:root { --cover-height: ${initialCoverCutoff()}; }`}</style>
<div
data-gb-page-cover
// data-cover-text-color={white or black} TODO: Retrieve from API for light & dark mode
data-cover-type={as}
data-full={String(as === 'full')}
className={tcls(
'overflow-hidden',
// Negative margin to balance the container padding
'-mx-4',

// Full-width cover: extend to edges, disregard TOC where possible
as === 'full' || as === 'background'
? [
'sm:-mx-6',
'md:-mx-8',
'lg:-ml-12',

// Extend the full-width cover
'layout-default:page-no-toc:lg:-ml-92', // Extend into the left sidebar if there's no TOC...
'layout-wide:2xl:-mr-[clamp(2rem,calc((100vw-90rem)/2+2rem),18rem)]', // ...and to the right if there's no outline.
'layout-wide:page-no-toc:2xl:-mx-[max(calc((100vw-90rem)/2+2rem),2rem)]', // Span full width if the page content is centered.
'layout-wide:has-sidebar:page-no-toc:lg:-ml-[max(calc((100vw-90rem)/2+23rem),23rem)]', // If there's still a sidebar, we have to factor it in too.

// Corner rounding: we round once the page is wide enough to have space around the cover.
'layout-default:2xl:rounded-corners:rounded-b-xl',
'layout-default:2xl:circular-corners:rounded-b-3xl',
'layout-wide:3xl:circular-corners:rounded-b-3xl',
'layout-wide:3xl:rounded-corners:rounded-b-xl',
// Round the bottom left corner once the sidebar is shown next to it
'has-sidebar:lg:rounded-corners:rounded-bl-xl',
'has-sidebar:lg:circular-corners:rounded-bl-3xl',
]
: null,

as === 'hero'
? [
// Regular cover: size regularly along with other content
CONTENT_STYLE,
'max-sm:-mx-4',
'sm:rounded-corners:rounded-xl',
'sm:circular-corners:rounded-3xl',
'mb-8',
'max-sm:w-screen',
'max-sm:-mt-8',
]
: null,

as === 'background'
? [
'-z-1 absolute inset-x-0 contrast-more:opacity-5 *:contrast-more:blur-md',
]
: null
)}
>
<PageCoverImage
imgs={{
light,
dark,
}}
y={cover.yPos}
height={height}
/>
</div>
</>
);
}
13 changes: 12 additions & 1 deletion packages/gitbook/src/components/PageBody/PageCoverImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ interface PageCoverImageProps {
y: number;
// Only if the `height` was customized by the user (and thus defined), we use it to set the cover's height and skip the default behaviour of fixed aspect-ratio.
height: number | undefined;
mask?: 'none' | 'radial';
}

export function PageCoverImage(props: PageCoverImageProps) {
const { imgs, y, height } = props;
const { imgs, y, height, mask } = props;
const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);

if (isLoading) {
Expand All @@ -53,6 +54,11 @@ export function PageCoverImage(props: PageCoverImageProps) {
: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
objectPosition: `50% ${objectPositionY}%`,
height, // if no height is passed, no height will be set.
maskComposite: 'intersect',
maskImage:
mask === 'radial'
? 'radial-gradient(200% 200% at 50% -100%, black 70%, rgba(0,0,0,0.85) 77.5%, rgba(0,0,0,0.6) 85%, rgba(0,0,0,0.1) 96.25%, transparent 100%), linear-gradient(to left, black 60%, rgba(0,0,0,0.8) 75%, rgba(0,0,0,0.1) 95%, transparent 100%), linear-gradient(to right, black 60%, rgba(0,0,0,0.8) 75%, rgba(0,0,0,0.1) 95%, transparent 100%)'
: undefined,
}}
/>
{imgs.dark && (
Expand All @@ -69,6 +75,11 @@ export function PageCoverImage(props: PageCoverImageProps) {
: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
objectPosition: `50% ${objectPositionY}%`,
height, // if no height is passed, no height will be set.
maskComposite: 'intersect',
maskImage:
mask === 'radial'
? 'radial-gradient(200% 200% at 50% -100%, black 70%, rgba(0,0,0,0.85) 77.5%, rgba(0,0,0,0.6) 85%, rgba(0,0,0,0.1) 96.25%, transparent 100%), linear-gradient(to left, black 60%, rgba(0,0,0,0.8) 75%, rgba(0,0,0,0.1) 95%, transparent 100%), linear-gradient(to right, black 60%, rgba(0,0,0,0.8) 75%, rgba(0,0,0,0.1) 95%, transparent 100%)'
: undefined,
}}
/>
)}
Expand Down
18 changes: 15 additions & 3 deletions packages/gitbook/src/components/PageBody/PageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export async function PageHeader(props: {
</div>

{hasAncestors && (
<nav aria-label="Breadcrumb" className="text-tint leading-snug">
<nav
aria-label="Breadcrumb"
className="page-cover-background:text-contrast-cover text-tint leading-snug page-cover-background:opacity-9"
>
<ol className="inline">
{ancestors.map((breadcrumb, index) => {
const href = linker.toPathForPage({
Expand Down Expand Up @@ -116,15 +119,24 @@ export async function PageHeader(props: {
'grow',
'text-pretty',
'clear-right',
'xs:clear-none'
'xs:clear-none',
'page-cover-background:text-contrast-cover'
)}
>
<PageIcon page={page} style={['text-tint-subtle ', 'shrink-0']} />
{page.title}
</h1>
) : null}
{page.description && page.layout.description ? (
<p className={tcls(CONTENT_STYLE_REDUCED, 'text-lg', 'text-tint', 'clear-both')}>
<p
className={tcls(
CONTENT_STYLE_REDUCED,
'text-lg',
'page-cover-background:text-contrast-cover',
'text-tint contrast-more:text-tint-strong',
'clear-both'
)}
>
{page.description}
</p>
) : null}
Expand Down
49 changes: 49 additions & 0 deletions packages/gitbook/src/components/RootLayout/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@
@apply leading-relaxed;
interpolate-size: allow-keywords; /* Opt-in for modern browsers to interpolate "auto" values in transitions/animations. */
overflow-x: hidden; /* We never want horizontal scroll of the whole page, it looks buggy and we should never have overflow anyway */
--cover-height: 0px;
--cover-text-top: rgb(var(--tint-12));
--cover-text-bottom: rgb(var(--tint-12));
}

:root:not(.dark):has(
[data-gb-page-cover][data-cover-type="background"][data-cover-text-color="white"]
),
:root.dark:has(
[data-gb-page-cover][data-cover-type="background"][data-cover-text-color="black"]
) {
--cover-text-top: rgb(var(--contrast-tint-12));
}

/* Modern browsers with `scrollbar-*` support */
Expand Down Expand Up @@ -282,6 +294,43 @@
}
}

@utility contrast-cover {
background-clip: border-box;
background-image: linear-gradient(
to bottom,
var(--cover-text-top) 0,
var(--cover-text-top) var(--cover-height),
var(--cover-text-bottom) var(--cover-height),
var(--cover-text-bottom) 100vh
);
background-repeat: no-repeat;
background-size: 100% 100vh;
background-attachment: fixed;
}

@utility text-contrast-cover {
color: transparent;
-webkit-text-fill-color: transparent;
background-image: linear-gradient(
to bottom,
var(--cover-text-top) 0,
var(--cover-text-top) var(--cover-height),
var(--cover-text-bottom) var(--cover-height),
var(--cover-text-bottom) 100vh
);
background-repeat: no-repeat;
background-size: 100% 100vh;
background-attachment: fixed;
background-clip: text;
-webkit-background-clip: text;

@media (prefers-contrast: more) {
color: var(--cover-text-bottom);
-webkit-text-fill-color: var(--cover-text-bottom);
background-image: none;
}
}

html {
color-scheme: light;

Expand Down
Loading
Loading