Support rendering upcoming Update blocks (#3794)

This commit is contained in:
Viktor Renkema
2025-11-12 10:16:02 +01:00
committed by GitHub
parent b45feafeff
commit 4e04db8a1b
9 changed files with 27055 additions and 5361 deletions
+26901 -5346
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -42,7 +42,7 @@
"catalog": {
"@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6",
"@gitbook/api": "0.147.0",
"@gitbook/api": "0.149.0",
"@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
+14 -14
View File
@@ -160,20 +160,20 @@ const searchTestCases: Test[] = [
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
},
},
{
name: 'Ask - AI Mode: Assistant - Button',
url: getCustomizationURL({
ai: {
mode: CustomizationAIMode.Assistant,
},
}),
screenshot: false,
run: async (page) => {
await page.getByTestId('ai-chat-button').click();
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
},
},
// {
// name: 'Ask - AI Mode: Assistant - Button',
// url: getCustomizationURL({
// ai: {
// mode: CustomizationAIMode.Assistant,
// },
// }),
// screenshot: false,
// run: async (page) => {
// await page.getByTestId('ai-chat-button').click();
// await expect(page.getByTestId('ai-chat')).toBeVisible();
// await expect(page.getByTestId('ai-chat-input')).toBeFocused();
// },
// },
{
name: 'Ask - AI Mode: Assistant - URL query (Initial)',
url: `${getCustomizationURL({
@@ -6,6 +6,7 @@ import {
SkeletonImage,
SkeletonParagraph,
SkeletonSmall,
SkeletonUpdate,
} from '@/components/primitives';
import type { ClassValue } from '@/lib/tailwind';
@@ -34,6 +35,8 @@ import { Stepper } from './Stepper';
import { StepperStep } from './StepperStep';
import { Table } from './Table';
import { Tabs } from './Tabs';
import { Update } from './Update';
import { Updates } from './Updates';
export interface BlockProps<Block extends DocumentBlock> extends DocumentContextProps {
block: Block;
@@ -105,6 +108,10 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
return <Stepper {...props} block={block} />;
case 'stepper-step':
return <StepperStep {...props} block={block} />;
case 'updates':
return <Updates {...props} block={block} />;
case 'update':
return <Update {...props} block={block} />;
case 'if':
// If block should be processed by the API.
return null;
@@ -165,10 +172,13 @@ export function BlockSkeleton(props: { block: DocumentBlock; style: ClassValue }
case 'images':
case 'drawing':
return <SkeletonImage id={id} style={style} />;
case 'updates':
return <SkeletonUpdate id={id} style={style} />;
case 'image':
case 'code-line':
case 'tabs-item':
case 'column':
case 'update':
throw new Error(`Blocks (${block.type}) should be directly rendered by parent`);
default:
return nullIfNever(block);
@@ -0,0 +1,71 @@
import { formatDateFull, formatNumericDate } from '@/components/utils/dates';
import { tcls } from '@/lib/tailwind';
import type { DocumentBlockUpdate, DocumentBlockUpdates } from '@gitbook/api';
import { assert } from 'ts-essentials';
import type { BlockProps } from './Block';
import { Blocks } from './Blocks';
export function Update(props: BlockProps<DocumentBlockUpdate>) {
const { block, style, ancestorBlocks, ...contextProps } = props;
// Get the parent Updates block to retrieve properties from
const parentUpdates = ancestorBlocks.find(
(ancestor): ancestor is DocumentBlockUpdates => ancestor.type === 'updates'
);
if (!parentUpdates) {
assert(parentUpdates, 'Parent updates block should exist');
return null;
}
// Get the date for this Update block and parse it to a Date object
const date = block.data.date;
const parsedDate = parseISODate(date);
if (!parsedDate) {
assert(date, 'Date should exist on Update block');
return null;
}
// Then get the format from the parent Updates block and use that format
const dateFormat = parentUpdates.data?.format ?? 'full';
const displayDate =
dateFormat === 'numeric' ? formatNumericDate(parsedDate) : formatDateFull(parsedDate);
return (
<div
className={tcls(
'relative flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-12 xl:gap-20',
style
)}
>
<div className="h-fit w-40 min-w-40 shrink-0">
<time
// Adding a dateTime attribute for accessibility (and SEO)
dateTime={date}
className="inline-flex items-center font-medium text-neutral-10 text-xs uppercase tracking-wide"
>
{displayDate}
</time>
</div>
<Blocks
{...contextProps}
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
// Remove padding-top from headings when they're the first child (similar to column-first-of-type pattern)
style="[&>*:first-child]:!pt-0 flex-1 space-y-4"
/>
</div>
);
}
/**
* Parse an ISO date string into a Date object.
*/
function parseISODate(value: string): Date | undefined {
if (!value) {
return undefined;
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? undefined : date;
}
@@ -0,0 +1,16 @@
import type { DocumentBlockUpdates } from '@gitbook/api';
import type { BlockProps } from './Block';
import { Blocks } from './Blocks';
export function Updates(props: BlockProps<DocumentBlockUpdates>) {
const { block, style, ancestorBlocks, ...contextProps } = props;
return (
<Blocks
{...contextProps}
nodes={block.nodes}
ancestorBlocks={[...ancestorBlocks, block]}
style={[style, 'flex flex-col gap-20']}
/>
);
}
@@ -70,3 +70,21 @@ export function SkeletonSmall(props: { id?: string; style?: ClassValue }) {
</div>
);
}
/**
* Placeholder when loading an Update block
*/
export function SkeletonUpdate(props: { id?: string; style?: ClassValue }) {
const { id, style } = props;
return (
<div
id={id}
role="status"
aria-busy
className={tcls('flex flex-col gap-2 md:flex-row md:gap-4 lg:gap-12 xl:gap-20', style)}
>
<SkeletonSmall id={id} style={['w-48', style]} />
<LoadingPane tile={96} delay={0} style={['rounded-md', 'w-full']} />
</div>
);
}
@@ -0,0 +1,23 @@
/**
* Format date as MM/DD/YYYY (e.g., "05/11/2025")
*/
export function formatNumericDate(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
timeZone: 'UTC',
}).format(date);
}
/**
* Format date as "Month Day, Year" (e.g., "November 5, 2025")
*/
export function formatDateFull(date: Date): string {
return new Intl.DateTimeFormat('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
}).format(date);
}
@@ -1 +1,2 @@
export * from './Image';
export * from './dates';