Skip to content

Commit 59da30f

Browse files
conico974Nicolas Dorseuil
andauthored
Add support for cover image repositioning (#3404)
Co-authored-by: Nicolas Dorseuil <[email protected]>
1 parent 2db7211 commit 59da30f

File tree

4 files changed

+133
-45
lines changed

4 files changed

+133
-45
lines changed

.changeset/rude-games-beg.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gitbook": patch
3+
---
4+
5+
Add support for cover repositioning

packages/gitbook/src/components/PageBody/PageCover.tsx

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ import type { GitBookSiteContext } from '@/lib/context';
22
import type { RevisionPageDocument, RevisionPageDocumentCover } from '@gitbook/api';
33
import type { StaticImageData } from 'next/image';
44

5-
import { Image, type ImageSize } from '@/components/utils';
6-
import { resolveContentRef } from '@/lib/references';
5+
import { getImageAttributes } from '@/components/utils';
6+
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
77
import { tcls } from '@/lib/tailwind';
88

9+
import { assert } from 'ts-essentials';
10+
import { PageCoverImage } from './PageCoverImage';
911
import defaultPageCoverSVG from './default-page-cover.svg';
1012

1113
const defaultPageCover = defaultPageCoverSVG as StaticImageData;
12-
const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 };
1314

1415
/**
1516
* Cover for the page.
@@ -26,6 +27,54 @@ export async function PageCover(props: {
2627
cover.refDark ? resolveContentRef(cover.refDark, context) : null,
2728
]);
2829

30+
const sizes = [
31+
// Cover takes the full width on mobile/table
32+
{
33+
media: '(max-width: 768px)',
34+
width: 768,
35+
},
36+
{
37+
media: '(max-width: 1024px)',
38+
width: 1024,
39+
},
40+
// Maximum size of the cover
41+
{ width: 1248 },
42+
];
43+
44+
const getImage = async (resolved: ResolvedContentRef | null, returnNull = false) => {
45+
if (!resolved && returnNull) return;
46+
const [attrs, size] = await Promise.all([
47+
getImageAttributes({
48+
sizes,
49+
source: resolved
50+
? {
51+
src: resolved.href,
52+
size: resolved.file?.dimensions ?? null,
53+
}
54+
: {
55+
src: defaultPageCover.src,
56+
size: {
57+
width: defaultPageCover.width,
58+
height: defaultPageCover.height,
59+
},
60+
},
61+
quality: 100,
62+
resize: context.imageResizer ?? false,
63+
}),
64+
context.imageResizer
65+
?.getImageSize(resolved?.href || defaultPageCover.src, {})
66+
.then((size) => size ?? undefined),
67+
]);
68+
return {
69+
...attrs,
70+
size,
71+
};
72+
};
73+
74+
const images = await Promise.all([getImage(resolved), getImage(resolvedDark, true)]);
75+
const [light, dark] = images;
76+
assert(light, 'Light image should be defined');
77+
2978
return (
3079
<div
3180
className={tcls(
@@ -52,47 +101,12 @@ export async function PageCover(props: {
52101
]
53102
)}
54103
>
55-
<Image
56-
alt="Page cover image"
57-
sources={{
58-
light: resolved
59-
? {
60-
src: resolved.href,
61-
size: resolved.file?.dimensions,
62-
}
63-
: {
64-
src: defaultPageCover.src,
65-
size: {
66-
width: defaultPageCover.width,
67-
height: defaultPageCover.height,
68-
},
69-
},
70-
dark: resolvedDark
71-
? {
72-
src: resolvedDark.href,
73-
size: resolvedDark.file?.dimensions,
74-
}
75-
: null,
104+
<PageCoverImage
105+
imgs={{
106+
light,
107+
dark,
76108
}}
77-
resize={
78-
// When using the default cover, we don't want to resize as it's a SVG
79-
resolved ? context.imageResizer : false
80-
}
81-
sizes={[
82-
// Cover takes the full width on mobile/table
83-
{
84-
media: '(max-width: 768px)',
85-
width: 768,
86-
},
87-
{
88-
media: '(max-width: 1024px)',
89-
width: 1024,
90-
},
91-
// Maximum size of the cover
92-
{ width: 1248 },
93-
]}
94-
className={tcls('w-full', 'object-cover', 'object-center')}
95-
inlineStyle={{ aspectRatio: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}` }}
109+
y={cover.yPos}
96110
/>
97111
</div>
98112
);
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
'use client';
2+
import { tcls } from '@/lib/tailwind';
3+
import { useRef } from 'react';
4+
import { useResizeObserver } from 'usehooks-ts';
5+
import type { ImageSize } from '../utils';
6+
7+
interface ImageAttributes {
8+
src: string;
9+
srcSet?: string;
10+
sizes?: string;
11+
width?: number;
12+
height?: number;
13+
size?: ImageSize;
14+
}
15+
16+
interface Images {
17+
light: ImageAttributes;
18+
dark?: ImageAttributes;
19+
}
20+
21+
const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 };
22+
23+
function getTop(container: { height?: number; width?: number }, y: number, img: ImageAttributes) {
24+
// When the size of the image hasn't been determined, we fallback to the center position
25+
if (!img.size || y === 0) return '50%';
26+
const ratio =
27+
container.height && container.width
28+
? Math.max(container.width / img.size.width, container.height / img.size.height)
29+
: 1;
30+
const scaledHeight = img.size ? img.size.height * ratio : PAGE_COVER_SIZE.height;
31+
const top =
32+
container.height && img.size ? (container.height - scaledHeight) / 2 + y * ratio : y;
33+
return `${top}px`;
34+
}
35+
36+
export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
37+
const containerRef = useRef<HTMLDivElement>(null);
38+
39+
const container = useResizeObserver({
40+
ref: containerRef,
41+
});
42+
43+
return (
44+
<div className="h-full w-full overflow-hidden" ref={containerRef}>
45+
<img
46+
src={imgs.light.src}
47+
fetchPriority="high"
48+
alt="Page cover"
49+
className={tcls('w-full', 'object-cover', imgs.dark ? 'dark:hidden' : '')}
50+
style={{
51+
aspectRatio: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
52+
objectPosition: `50% ${getTop(container, y, imgs.light)}`,
53+
}}
54+
/>
55+
{imgs.dark && (
56+
<img
57+
src={imgs.dark.src}
58+
fetchPriority="low"
59+
alt="Page cover"
60+
className={tcls('w-full', 'object-cover', 'dark:inline', 'hidden')}
61+
style={{
62+
aspectRatio: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
63+
objectPosition: `50% ${getTop(container, y, imgs.dark)}`,
64+
}}
65+
/>
66+
)}
67+
</div>
68+
);
69+
}

packages/gitbook/src/components/utils/Image.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type ImageSource = {
1616
aspectRatio?: string;
1717
};
1818

19-
type ImageSourceSized = {
19+
export type ImageSourceSized = {
2020
src: string;
2121
size: ImageSize | null;
2222
aspectRatio?: string;
@@ -244,7 +244,7 @@ async function ImagePictureSized(
244244
* Get the attributes for an image.
245245
* src, srcSet, sizes, width, height, etc.
246246
*/
247-
async function getImageAttributes(params: {
247+
export async function getImageAttributes(params: {
248248
sizes: ImageResponsiveSize[];
249249
source: ImageSourceSized;
250250
quality: number;

0 commit comments

Comments
 (0)