Support new coverDefinitionDark for cards & image type (#3547)

This commit is contained in:
Nolann B.
2025-08-08 18:08:11 +02:00
committed by GitHub
parent 87841124e0
commit ff96bb5787
6 changed files with 161 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'gitbook': patch
---
Support new coverDefinitionDark for cards & image type
+2 -2
View File
@@ -246,7 +246,7 @@
"react-dom": "^19.0.0",
},
"catalog": {
"@gitbook/api": "^0.132.0",
"@gitbook/api": "^0.133.0",
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-0M+qjp+clUD0R1E5eWQFhxEvWLNaOtGQRUaBn8CUABnSKredagq92hUS9VjOzGsTm37xLfpaxl97AVtbeOsHew=="],
@@ -611,7 +611,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@6.6.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "6.6.0" } }, "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg=="],
"@gitbook/api": ["@gitbook/api@0.132.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-1kuYIHiQIFzcSbKZ0JkzfSKujWFHyM4OAT3P0JENekBffwEinUMqblkJZNQmWA4QhBYi0QT8bJA6OCda4Xx0oA=="],
"@gitbook/api": ["@gitbook/api@0.133.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-Xi/A8NROQIj1haDA2XYr9b2+suUviyEVjplSslmwA2HyZnFNlkTaB1W0CwpVrZF8FZsfi3fZekQECOF34RLDDA=="],
"@gitbook/cache-tags": ["@gitbook/cache-tags@workspace:packages/cache-tags"],
+1 -1
View File
@@ -34,7 +34,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.132.0"
"@gitbook/api": "^0.133.0"
}
},
"patchedDependencies": {
@@ -1,18 +1,16 @@
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import {
type ContentRef,
type DocumentTableViewCards,
SiteInsightsLinkPosition,
} from '@gitbook/api';
import { LinkBox, LinkOverlay } from '@/components/primitives';
import { Image } from '@/components/utils';
import { resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import { RecordColumnValue } from './RecordColumnValue';
import type { TableRecordKV, TableViewProps } from './Table';
import { RecordCardStyles } from './styles';
import { getRecordValue } from './utils';
import { getRecordCardCovers } from './utils';
export async function RecordCard(
props: TableViewProps<DocumentTableViewCards> & {
@@ -21,25 +19,21 @@ export async function RecordCard(
) {
const { view, record, context, block, isOffscreen } = props;
const coverFile = view.coverDefinition
? getRecordValue<string[]>(record[1], view.coverDefinition)?.[0]
: null;
const { dark, light } = getRecordCardCovers(record[1], view);
const targetRef = view.targetDefinition
? (record[1].values[view.targetDefinition] as ContentRef)
: null;
const [cover, target] = await Promise.all([
coverFile && context.contentContext
? resolveContentRef({ kind: 'file', file: coverFile }, context.contentContext)
: null,
const [lightCover, darkCover, target] = await Promise.all([
light && context.contentContext ? resolveContentRef(light, context.contentContext) : null,
dark && context.contentContext ? resolveContentRef(dark, context.contentContext) : null,
targetRef && context.contentContext
? resolveContentRef(targetRef, context.contentContext)
: null,
]);
const coverIsSquareOrPortrait =
cover?.file?.dimensions &&
cover.file?.dimensions?.width / cover.file?.dimensions?.height <= 1;
const darkCoverIsSquareOrPortrait = isSquareOrPortrait(darkCover);
const lightCoverIsSquareOrPortrait = isSquareOrPortrait(lightCover);
const body = (
<div
@@ -63,23 +57,32 @@ export async function RecordCard(
// On mobile, check if we can display the cover responsively or not:
// - If the file has a landscape aspect ratio, we display it normally
// - If the file is square or portrait, we display it left with 40% of the card width
coverIsSquareOrPortrait
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
? [
'grid-cols-[40%__1fr]',
'min-[432px]:grid-cols-none',
'min-[432px]:grid-rows-[auto_1fr]',
]
lightCoverIsSquareOrPortrait
? 'grid-cols-[40%__1fr] min-[432px]:grid-cols-none min-[432px]:grid-rows-[auto_1fr]'
: '',
darkCoverIsSquareOrPortrait
? 'dark:grid-cols-[40%__1fr] dark:min-[432px]:grid-cols-none dark:min-[432px]:grid-rows-[auto_1fr]'
: '',
].filter(Boolean)
: 'grid-rows-[auto_1fr]'
)}
>
{cover ? (
{lightCover ? (
<Image
alt="Cover"
sources={{
light: {
src: cover.href,
size: cover.file?.dimensions,
src: lightCover.href,
size: lightCover.file?.dimensions,
},
dark: darkCover
? {
src: darkCover.href,
size: darkCover.file?.dimensions,
}
: null,
}}
sizes={[
{
@@ -92,8 +95,15 @@ export async function RecordCard(
'w-full',
'h-full',
'object-cover',
coverIsSquareOrPortrait
? ['min-[432px]:h-auto', 'min-[432px]:aspect-video']
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
? [
lightCoverIsSquareOrPortrait
? 'min-[432px]:aspect-video min-[432px]:h-auto'
: '',
darkCoverIsSquareOrPortrait
? 'dark:min-[432px]:aspect-video dark:min-[432px]:h-auto'
: '',
].filter(Boolean)
: ['h-auto', 'aspect-video']
)}
priority={isOffscreen ? 'lazy' : 'high'}
@@ -167,3 +177,16 @@ export async function RecordCard(
return <div className={tcls(RecordCardStyles)}>{body}</div>;
}
/**
* Check if a file is square or portrait.
*/
function isSquareOrPortrait(contentRef: ResolvedContentRef | null) {
const file = contentRef?.file;
if (!file || !file.dimensions) {
return false;
}
return file.dimensions?.width / file.dimensions?.height <= 1;
}
@@ -174,7 +174,6 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
<StyledLink
key={index}
href={ref.href}
target="_blank"
className="flex flex-row items-center gap-2"
insights={
ref.file
@@ -329,6 +328,50 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
</Tag>
);
}
case 'image': {
const image = context.contentContext
? await resolveContentRef(value as ContentRef, context.contentContext)
: null;
if (!image) {
return null;
}
return (
<Tag className={tcls('text-base')} aria-labelledby={ariaLabelledBy}>
<StyledLink
href={image.href}
className="flex flex-row items-center gap-2"
insights={
image.file
? {
type: 'link_click',
link: {
target: value as ContentRef,
position: SiteInsightsLinkPosition.Content,
},
}
: undefined
}
>
<Image
style={['max-h-lh', 'h-lh']}
alt={image.text}
sizes={[{ width: 24 }]}
resize={context.contentContext?.imageResizer}
sources={{
light: {
src: image.href,
size: image.file?.dimensions,
},
}}
priority="lazy"
/>
{image.text}
</StyledLink>
</Tag>
);
}
default:
assertNever(definition);
}
@@ -1,4 +1,11 @@
import type { ContentRef, DocumentTableDefinition, DocumentTableRecord } from '@gitbook/api';
import type {
ContentRef,
ContentRefFile,
ContentRefURL,
DocumentTableDefinition,
DocumentTableRecord,
DocumentTableViewCards,
} from '@gitbook/api';
import assertNever from 'assert-never';
/**
@@ -12,6 +19,57 @@ export function getRecordValue<T extends number | string | boolean | string[] |
return record.values[definitionId];
}
/**
* Get the covers for a record card.
* Returns both the light and dark covers.
* The light cover is a string or a content ref (image or files column type).
* The dark cover is a content ref (image column type).
*/
export function getRecordCardCovers(
record: DocumentTableRecord,
view: DocumentTableViewCards
): { [key in 'light' | 'dark']: ContentRefFile | ContentRefURL | null } {
return {
light: (() => {
if (!view.coverDefinition) {
return null;
}
const value = getRecordValue(record, view.coverDefinition) as
| ContentRefFile
| ContentRefURL
| string[];
if (Array.isArray(value)) {
if (value.length === 0) {
return null;
}
if (typeof value[0] === 'string') {
return { kind: 'file', file: value[0] };
}
}
return value as ContentRefFile | ContentRefURL;
})(),
dark: (() => {
if (!view.coverDefinitionDark) {
return null;
}
const value = getRecordValue(record, view.coverDefinitionDark) as
| ContentRefFile
| ContentRefURL;
if (!value) {
return null;
}
return value;
})(),
};
}
/**
* Get the text alignment for a column.
*/