Revert "Support cover image heights and positioning changes" (#3771)

This commit is contained in:
Viktor Renkema
2025-10-30 10:24:22 +01:00
committed by GitHub
parent 1d1f0a7c6a
commit 3aea302648
8 changed files with 37 additions and 182 deletions
+5 -5
View File
@@ -112,11 +112,11 @@ const testCases: TestsCase[] = [
contentBaseURL: 'https://adiblar.gitbook.io',
tests: [{ name: 'Home', url: '/' }],
},
{
name: 'docs.gradient.network',
contentBaseURL: 'https://docs.gradient.network',
tests: [{ name: 'Home', url: '/' }],
},
// {
// name: 'docs.gradient.network',
// contentBaseURL: 'https://docs.gradient.network',
// tests: [{ name: 'Home', url: '/' }],
// },
// {
// name: 'mygate-network.gitbook.io',
// contentBaseURL: 'https://mygate-network.gitbook.io',
+3 -13
View File
@@ -33,7 +33,6 @@ import {
headerLinks,
runTestCases,
waitForCookiesDialog,
waitForCoverImages,
waitForNotFound,
} from './util';
@@ -907,10 +906,7 @@ const testCases: TestsCase[] = [
{
name: 'With cover',
url: 'page-options/page-with-cover',
run: async (page) => {
await waitForCookiesDialog(page);
await waitForCoverImages(page);
},
run: waitForCookiesDialog,
},
{
name: 'With cover for dark mode',
@@ -925,18 +921,12 @@ const testCases: TestsCase[] = [
{
name: 'With hero cover',
url: 'page-options/page-with-hero-cover',
run: async (page) => {
await waitForCookiesDialog(page);
await waitForCoverImages(page);
},
run: waitForCookiesDialog,
},
{
name: 'With cover and no TOC',
url: 'page-options/page-with-cover-and-no-toc',
run: async (page) => {
await waitForCookiesDialog(page);
await waitForCoverImages(page);
},
run: waitForCookiesDialog,
screenshot: {
waitForTOCScrolling: false,
},
-7
View File
@@ -154,13 +154,6 @@ export async function waitForNotFound(_page: Page, response: Response | null) {
expect(response?.status()).toBe(404);
}
export async function waitForCoverImages(page: Page) {
// Wait for cover images to exist (not the shimmer placeholder)
await expect(page.locator('img[alt="Page cover"]').first()).toBeVisible({
timeout: 10_000,
});
}
/**
* Transform test cases into Playwright tests and run it.
*/
@@ -8,7 +8,6 @@ import { tcls } from '@/lib/tailwind';
import { assert } from 'ts-essentials';
import { PageCoverImage } from './PageCoverImage';
import { getCoverHeight } from './coverHeight';
import defaultPageCoverSVG from './default-page-cover.svg';
const defaultPageCover = defaultPageCoverSVG as StaticImageData;
@@ -23,12 +22,6 @@ export async function PageCover(props: {
context: GitBookSiteContext;
}) {
const { as, page, cover, context } = props;
const height = getCoverHeight(cover);
if (!height) {
return null;
}
const [resolved, resolvedDark] = await Promise.all([
cover.ref ? resolveContentRef(cover.ref, context) : null,
cover.refDark ? resolveContentRef(cover.refDark, context) : null,
@@ -85,7 +78,6 @@ export async function PageCover(props: {
<div
id="page-cover"
data-full={String(as === 'full')}
style={{ height }}
className={tcls(
'overflow-hidden',
// Negative margin to balance the container padding
@@ -1,7 +1,8 @@
'use client';
import { tcls } from '@/lib/tailwind';
import { useRef } from 'react';
import { useResizeObserver } from 'usehooks-ts';
import type { ImageSize } from '../utils';
import { useCoverPosition } from './useCoverPosition';
interface ImageAttributes {
src: string;
@@ -17,16 +18,28 @@ interface Images {
dark?: ImageAttributes;
}
export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
const { containerRef, objectPositionY, isLoading } = useCoverPosition(imgs, y);
const PAGE_COVER_SIZE: ImageSize = { width: 1990, height: 480 };
if (isLoading) {
return (
<div className="h-full w-full overflow-hidden" ref={containerRef}>
<div className="h-full w-full animate-pulse bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900" />
</div>
);
}
function getTop(container: { height?: number; width?: number }, y: number, img: ImageAttributes) {
// When the size of the image hasn't been determined, we fallback to the center position
if (!img.size || y === 0) return '50%';
const ratio =
container.height && container.width
? Math.max(container.width / img.size.width, container.height / img.size.height)
: 1;
const scaledHeight = img.size ? img.size.height * ratio : PAGE_COVER_SIZE.height;
const top =
container.height && img.size ? (container.height - scaledHeight) / 2 + y * ratio : y;
return `${top}px`;
}
export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
const containerRef = useRef<HTMLDivElement>(null);
const container = useResizeObserver({
// @ts-expect-error wrong types
ref: containerRef,
});
return (
<div className="h-full w-full overflow-hidden" ref={containerRef}>
@@ -36,9 +49,10 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
sizes={imgs.light.sizes}
fetchPriority="high"
alt="Page cover"
className={tcls('h-full', 'w-full', 'object-cover', imgs.dark ? 'dark:hidden' : '')}
className={tcls('w-full', 'object-cover', imgs.dark ? 'dark:hidden' : '')}
style={{
objectPosition: `50% ${objectPositionY}%`,
aspectRatio: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
objectPosition: `50% ${getTop(container, y, imgs.light)}`,
}}
/>
{imgs.dark && (
@@ -48,9 +62,10 @@ export function PageCoverImage({ imgs, y }: { imgs: Images; y: number }) {
sizes={imgs.dark.sizes}
fetchPriority="low"
alt="Page cover"
className={tcls('h-full', 'w-full', 'object-cover', 'dark:inline', 'hidden')}
className={tcls('w-full', 'object-cover', 'dark:inline', 'hidden')}
style={{
objectPosition: `50% ${objectPositionY}%`,
aspectRatio: `${PAGE_COVER_SIZE.width}/${PAGE_COVER_SIZE.height}`,
objectPosition: `50% ${getTop(container, y, imgs.dark)}`,
}}
/>
)}
@@ -1,25 +0,0 @@
import type { RevisionPageDocumentCover } from '@gitbook/api';
export const DEFAULT_COVER_HEIGHT = 240;
export const MIN_COVER_HEIGHT = 10;
export const MAX_COVER_HEIGHT = 700;
// Normalize and clamp the cover height between the minimum and maximum heights
function clampCoverHeight(height: number | null | undefined): number {
if (typeof height !== 'number' || Number.isNaN(height)) {
return DEFAULT_COVER_HEIGHT;
}
return Math.min(MAX_COVER_HEIGHT, Math.max(MIN_COVER_HEIGHT, height));
}
export function getCoverHeight(
cover: RevisionPageDocumentCover | null | undefined
): number | undefined {
// Cover (and thus height) is not defined
if (!cover) {
return undefined;
}
return clampCoverHeight((cover as RevisionPageDocumentCover).height ?? DEFAULT_COVER_HEIGHT);
}
@@ -1,3 +1,2 @@
export * from './PageBody';
export * from './PageCover';
export * from './useCoverPosition';
@@ -1,109 +0,0 @@
'use client';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useResizeObserver } from 'usehooks-ts';
interface ImageSize {
width: number;
height: number;
}
interface ImageAttributes {
src: string;
srcSet?: string;
sizes?: string;
width?: number;
height?: number;
size?: ImageSize;
}
interface Images {
light: ImageAttributes;
dark?: ImageAttributes;
}
/**
* Hook to calculate the object position Y percentage for a cover image
* based on the y offset, image dimensions, and container dimensions.
*/
export function useCoverPosition(imgs: Images, y: number) {
const containerRef = useRef<HTMLDivElement>(null);
const [loadedDimensions, setLoadedDimensions] = useState<ImageSize | null>(null);
const [isLoading, setIsLoading] = useState(!imgs.light.size && !imgs.dark?.size);
const container = useResizeObserver({
// @ts-expect-error wrong types
ref: containerRef,
});
// Load original image dimensions if not provided in `imgs`
useLayoutEffect(() => {
// Check if we have dimensions from either light or dark image
const hasDimensions = imgs.light.size || imgs.dark?.size;
if (hasDimensions) {
return; // Already have dimensions
}
setIsLoading(true);
// Load the original image (using src, not srcSet) to get true dimensions
// Use dark image if available, otherwise fall back to light
const imageToLoad = imgs.dark || imgs.light;
const img = new Image();
img.onload = () => {
setLoadedDimensions({
width: img.naturalWidth,
height: img.naturalHeight,
});
setIsLoading(false);
};
img.onerror = () => {
// If image fails to load, use a fallback
setIsLoading(false);
};
img.src = imageToLoad.src;
}, [imgs.light, imgs.dark]);
// Use provided dimensions or fall back to loaded dimensions
// Check light first, then dark, then loaded dimensions
const imageDimensions = imgs.light.size ?? imgs.dark?.size ?? loadedDimensions;
// Calculate ratio and dimensions similar to useCoverPosition hook
const ratio =
imageDimensions && container.height && container.width
? Math.max(
container.width / imageDimensions.width,
container.height / imageDimensions.height
)
: 1;
const safeRatio = ratio || 1;
const scaledHeight =
imageDimensions && container.height ? imageDimensions.height * safeRatio : null;
const maxOffset =
scaledHeight && container.height
? Math.max(0, (scaledHeight - container.height) / 2 / safeRatio)
: 0;
// Parse the position between the allowed min/max
const objectPositionY = useMemo(() => {
if (!container.height || !imageDimensions) {
return 50;
}
const scaled = imageDimensions.height * safeRatio;
if (scaled <= container.height || maxOffset === 0) {
return 50;
}
const clampedOffset = Math.max(-maxOffset, Math.min(maxOffset, y));
const relative = (maxOffset - clampedOffset) / (2 * maxOffset);
return relative * 100;
}, [container.height, imageDimensions, maxOffset, safeRatio, y]);
return {
containerRef,
objectPositionY,
isLoading: !imageDimensions || isLoading,
};
}