mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Revert "Support cover image heights and positioning changes" (#3771)
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user