Skip to content
Merged
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
4 changes: 2 additions & 2 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@
"react-dom": "^19.0.0",
},
"catalog": {
"@gitbook/api": "^0.138.0",
"@gitbook/api": "^0.139.0",
"bidc": "^0.0.2",
},
"packages": {
Expand Down Expand Up @@ -661,7 +661,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.138.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-6jYH1R5IpmbFj3qUyDGuUBaABCinKQcqNCtTKtql0MSPaFp9KVnqyBsIaq/s9HohjCpyU1/EoddwzAaJWHWkkw=="],
"@gitbook/api": ["@gitbook/api@0.139.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-6VqN4BVvOdaRng2xz5wh0gj6NJmJJZwtswKZ6g9RMWedp2UkeMXi+kcO3fBo2VYJsxhIgbtWikqGEePgHTx67g=="],

"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"workspaces": {
"packages": ["packages/*"],
"catalog": {
"@gitbook/api": "^0.138.0",
"@gitbook/api": "^0.139.0",
"bidc": "^0.0.2"
}
},
Expand Down
34 changes: 30 additions & 4 deletions packages/gitbook/src/components/DocumentView/Table/RecordCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Image } from '@/components/utils';
import { type ResolvedContentRef, resolveContentRef } from '@/lib/references';
import { tcls } from '@/lib/tailwind';
import {
CardsImageObjectFit,
type ContentRef,
type DocumentTableViewCards,
SiteInsightsLinkPosition,
Expand All @@ -25,8 +26,12 @@ export async function RecordCard(
: null;

const [lightCover, darkCover, target] = await Promise.all([
light && context.contentContext ? resolveContentRef(light, context.contentContext) : null,
dark && context.contentContext ? resolveContentRef(dark, context.contentContext) : null,
light.contentRef && context.contentContext
? resolveContentRef(light.contentRef, context.contentContext)
: null,
dark.contentRef && context.contentContext
? resolveContentRef(dark.contentRef, context.contentContext)
: null,
targetRef && context.contentContext
? resolveContentRef(targetRef, context.contentContext)
: null,
Expand All @@ -35,6 +40,10 @@ export async function RecordCard(
const darkCoverIsSquareOrPortrait = isSquareOrPortrait(darkCover);
const lightCoverIsSquareOrPortrait = isSquareOrPortrait(lightCover);

const darkObjectFit = dark.objectFit ? `dark:${getObjectFitClass(dark.objectFit)}` : '';
const lightObjectFit = light.objectFit ? getObjectFitClass(light.objectFit) : '';
const objectFits = `${lightObjectFit} ${darkObjectFit}`;

const body = (
<div
className={tcls(
Expand Down Expand Up @@ -94,7 +103,7 @@ export async function RecordCard(
'min-w-0',
'w-full',
'h-full',
'object-cover',
'bg-tint-subtle',
lightCoverIsSquareOrPortrait || darkCoverIsSquareOrPortrait
? [
lightCoverIsSquareOrPortrait
Expand All @@ -104,7 +113,8 @@ export async function RecordCard(
? 'dark:min-[432px]:aspect-video dark:min-[432px]:h-auto'
: '',
].filter(Boolean)
: ['h-auto', 'aspect-video']
: ['h-auto', 'aspect-video'],
objectFits
)}
priority={isOffscreen ? 'lazy' : 'high'}
preload
Expand Down Expand Up @@ -190,3 +200,19 @@ function isSquareOrPortrait(contentRef: ResolvedContentRef | null) {

return file.dimensions?.width / file.dimensions?.height <= 1;
}

/**
* Get the CSS class for object-fit based on the objectFit value.
*/
function getObjectFitClass(objectFit: CardsImageObjectFit): string {
switch (objectFit) {
case CardsImageObjectFit.Contain:
return 'object-contain';
case CardsImageObjectFit.Fill:
return 'object-fill';
case CardsImageObjectFit.Cover:
return 'object-cover';
default:
throw new Error(`Unsupported object fit: ${objectFit}`);
}
}
91 changes: 52 additions & 39 deletions packages/gitbook/src/components/DocumentView/Table/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type {
CardsImageObjectFit,
ContentRef,
ContentRefFile,
ContentRefURL,
DocumentTableDefinition,
DocumentTableImageRecord,
DocumentTableRecord,
DocumentTableViewCards,
} from '@gitbook/api';
Expand All @@ -21,55 +23,66 @@ export function getRecordValue<T extends number | string | boolean | string[] |

/**
* Get the covers for a record card.
* Returns both the light and dark covers.
* Returns both the light and dark covers with their content refs and optional object fit.
* 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 } {
): {
[key in 'light' | 'dark']: {
contentRef: ContentRefFile | ContentRefURL | null;
objectFit?: CardsImageObjectFit;
};
} {
const lightValue = view.coverDefinition
? (getRecordValue(record, view.coverDefinition) as DocumentTableImageRecord | string[])
: null;

const darkValue = view.coverDefinitionDark
? (getRecordValue(record, view.coverDefinitionDark) as DocumentTableImageRecord)
: 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;
})(),
light: processCoverValue(lightValue),
dark: processCoverValue(darkValue),
};
}

/**
* Process a cover value and return the content ref and object fit.
*/
function processCoverValue(value: DocumentTableImageRecord | string[] | null | undefined): {
contentRef: ContentRefFile | ContentRefURL | null;
objectFit?: CardsImageObjectFit;
} {
if (!value) {
return { contentRef: null };
}

if (Array.isArray(value)) {
if (value.length === 0) {
return { contentRef: null };
}

if (typeof value[0] === 'string') {
return { contentRef: { kind: 'file', file: value[0] } };
}
}

// Check if it's the new schema with objectFit
if (value && typeof value === 'object' && 'ref' in value && 'objectFit' in value) {
return {
contentRef: value.ref,
objectFit: value.objectFit,
};
}

// It's a direct ContentRef
return { contentRef: value as ContentRefFile | ContentRefURL };
}

/**
* Get the text alignment for a column.
*/
Expand Down