Skip to content

Commit c054650

Browse files
committed
Merge remote-tracking branch 'origin/main' into brett/RND-5227-site-tabs
2 parents 32ac1e3 + 2982b6a commit c054650

File tree

24 files changed

+408
-121
lines changed

24 files changed

+408
-121
lines changed

.changeset/bright-cars-explain.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Add automatic color contrast in site header, restyle search button

.changeset/nice-waves-compare.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'gitbook': patch
3+
---
4+
5+
Update ogimage with new design

bun.lockb

392 Bytes
Binary file not shown.

packages/gitbook/.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,5 @@ screenshots/
3232
/playwright/.cache/
3333

3434
# Generated public files
35-
/public/~gitbook/static/
35+
/public/~gitbook/static/*
36+
!/public/~gitbook/static/images

packages/gitbook/e2e/pages.spec.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,10 @@ const testCases: TestsCase[] = [
395395
await page.click('[data-testid="annotation-button"]');
396396
},
397397
},
398+
{
399+
name: 'Stepper',
400+
url: 'blocks/stepper',
401+
},
398402
],
399403
},
400404
{
@@ -495,7 +499,7 @@ const testCases: TestsCase[] = [
495499
tests: [
496500
{
497501
name: 'Valid link',
498-
url: 'TGs8PkF4GWVtbmPnWhYL/',
502+
url: 'thDznyWXCeEoT55WB7HC/',
499503
},
500504
{
501505
name: 'Invalid link',

packages/gitbook/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"clean": "rm -rf ./.next && rm -rf ./public/~gitbook/static"
1717
},
1818
"dependencies": {
19-
"@gitbook/api": "^0.66.0",
19+
"@gitbook/api": "^0.69.0",
2020
"@gitbook/cache-do": "workspace:*",
2121
"@gitbook/emoji-codepoints": "workspace:*",
2222
"@gitbook/icons": "workspace:*",
Loading
Loading

packages/gitbook/src/app/(site)/(content)/[[...pathname]]/not-found.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getSpaceLanguage, t } from '@/intl/server';
2-
import { getSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
2+
import { getSiteLayoutData } from '@/lib/api';
33
import { getSiteContentPointer } from '@/lib/pointer';
44
import { tcls } from '@/lib/tailwind';
55

Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { CustomizationHeaderPreset } from '@gitbook/api';
12
import { redirect } from 'next/navigation';
23
import { ImageResponse } from 'next/og';
34
import { NextRequest } from 'next/server';
5+
import colorContrast from 'postcss-color-contrast/js';
46
import React from 'react';
57

8+
import { absoluteHref } from '@/lib/links';
9+
import { tcls } from '@/lib/tailwind';
610
import { getContentTitle } from '@/lib/utils';
711

812
import { PageIdParams, fetchPageData } from '../../../../fetch';
@@ -14,40 +18,206 @@ export const runtime = 'edge';
1418
*/
1519
export async function GET(req: NextRequest, { params }: { params: PageIdParams }) {
1620
const { space, page, customization, site } = await fetchPageData(params);
17-
const url = new URL(space.urls.published ?? space.urls.app);
1821

1922
if (customization.socialPreview.url) {
2023
// If user configured a custom social preview, we redirect to it.
2124
redirect(customization.socialPreview.url);
2225
}
2326

27+
// TODO: Support all fonts available in GitBook
28+
// Right now this is impossible since next/font/google does not expose the cached font file
29+
// Another option would be to use the Satori prop `loadAdditionalAsset` [example](https://github.com/vercel/satori/blob/main/playground/pages/index.tsx),
30+
// but this prop isn't (yet) exposed through `ImageResponse`.
31+
const interRegular = await fetch(
32+
new URL('../../../../../../fonts/Inter/Inter-Regular.ttf', import.meta.url),
33+
).then((res) => res.arrayBuffer());
34+
const interBold = await fetch(
35+
new URL('../../../../../../fonts/Inter/Inter-Bold.ttf', import.meta.url),
36+
).then((res) => res.arrayBuffer());
37+
38+
const theme = customization.themes.default;
39+
const useLightTheme = theme === 'light';
40+
41+
// We have no access to CSS variables, so we'll have to hardcode some values
42+
const baseColors = {
43+
light: '#ffffff',
44+
dark: '#111827',
45+
};
46+
47+
let colors = {
48+
background: baseColors[theme],
49+
gradient: customization.styling.primaryColor[theme],
50+
title: customization.styling.primaryColor[theme],
51+
body: baseColors[useLightTheme ? 'dark' : 'light'], // Invert text on background
52+
};
53+
54+
const gridWhite = absoluteHref('~gitbook/static/images/ogimage-grid-white.png', true);
55+
const gridBlack = absoluteHref('~gitbook/static/images/ogimage-grid-black.png', true);
56+
57+
let gridAsset = useLightTheme ? gridBlack : gridWhite;
58+
59+
switch (customization.header.preset) {
60+
case CustomizationHeaderPreset.Custom:
61+
colors = {
62+
background: customization.header.backgroundColor?.[theme] || colors.background,
63+
gradient: customization.header.linkColor?.[theme] || colors.gradient,
64+
title: customization.header.linkColor?.[theme] || colors.title,
65+
body: colorContrast(
66+
customization.header.backgroundColor?.[theme] || colors.background,
67+
[baseColors.light, baseColors.dark],
68+
),
69+
};
70+
gridAsset = colors.body == baseColors.light ? gridWhite : gridBlack;
71+
break;
72+
73+
case CustomizationHeaderPreset.Bold:
74+
colors = {
75+
background: customization.styling.primaryColor[theme],
76+
gradient: colorContrast(customization.styling.primaryColor[theme], [
77+
baseColors.light,
78+
baseColors.dark,
79+
]),
80+
title: colorContrast(customization.styling.primaryColor[theme], [
81+
baseColors.light,
82+
baseColors.dark,
83+
]),
84+
body: colorContrast(customization.styling.primaryColor[theme], [
85+
baseColors.light,
86+
baseColors.dark,
87+
]),
88+
};
89+
gridAsset = colors.body == baseColors.light ? gridWhite : gridBlack;
90+
break;
91+
}
92+
93+
const favicon = function () {
94+
if ('icon' in customization.favicon)
95+
return (
96+
<img
97+
src={customization.favicon.icon[theme]}
98+
width={40}
99+
height={40}
100+
tw={tcls('mr-4')}
101+
alt="Icon"
102+
/>
103+
);
104+
if ('emoji' in customization.favicon)
105+
return (
106+
<span tw={tcls('text-4xl', 'mr-4')}>
107+
{String.fromCodePoint(parseInt('0x' + customization.favicon.emoji))}
108+
</span>
109+
);
110+
return (
111+
<img
112+
src={absoluteHref(
113+
`~gitbook/icon?size=medium&theme=${customization.themes.default}`,
114+
true,
115+
)}
116+
alt="Icon"
117+
width={40}
118+
height={40}
119+
tw={tcls('mr-4')}
120+
/>
121+
);
122+
};
123+
24124
return new ImageResponse(
25125
(
26126
<div
27-
tw="bg-gray-50 py-16 px-14"
28-
style={{
29-
height: '100%',
30-
width: '100%',
31-
display: 'flex',
32-
flexDirection: 'column',
33-
}}
127+
tw={tcls(
128+
'justify-between',
129+
'p-20',
130+
'relative',
131+
'w-full',
132+
'h-full',
133+
'flex',
134+
'flex-col',
135+
`bg-[${colors.background}]`,
136+
`text-[${colors.body}]`,
137+
)}
34138
>
35-
<h2 tw="text-7xl font-bold tracking-tight text-left">
36-
{getContentTitle(space, customization, site ?? null)}
37-
</h2>
38-
<div tw="flex flex-1">
39-
<p tw="text-4xl">{page ? page.title : 'Not found'}</p>
40-
</div>
41-
<div tw="flex">
42-
<p tw="text-4xl">
43-
{url.hostname + (url.pathname.length > 1 ? url.pathname : '')}
44-
</p>
139+
{/* Gradient */}
140+
<div
141+
tw={tcls('absolute', 'inset-0')}
142+
style={{
143+
backgroundImage: `radial-gradient(ellipse 100% 100% at top right , ${colors.gradient}, ${colors.gradient}00)`,
144+
opacity: 0.5,
145+
}}
146+
></div>
147+
148+
{/* Grid */}
149+
<img
150+
tw={tcls('absolute', 'inset-0', 'w-[100vw]', 'h-[100vh]')}
151+
src={gridAsset}
152+
alt="Grid"
153+
/>
154+
155+
{/* Logo */}
156+
{customization.header.logo ? (
157+
<img
158+
alt="Logo"
159+
height={60}
160+
src={
161+
useLightTheme
162+
? customization.header.logo.light
163+
: customization.header.logo.dark
164+
}
165+
/>
166+
) : (
167+
<div tw={tcls('flex')}>
168+
{favicon()}
169+
<h3 tw={tcls('text-4xl', 'my-0')}>
170+
{getContentTitle(space, customization, site ?? null)}
171+
</h3>
172+
</div>
173+
)}
174+
175+
{/* Title and description */}
176+
<div tw={tcls('flex', 'flex-col')}>
177+
<h1
178+
tw={tcls(
179+
'text-8xl',
180+
'my-0',
181+
'tracking-tight',
182+
'leading-none',
183+
'text-left',
184+
`text-[${colors.title}]`,
185+
'font-bold',
186+
)}
187+
>
188+
{page
189+
? page.title.length > 64
190+
? page.title.slice(0, 64) + '...'
191+
: page.title
192+
: 'Not found'}
193+
</h1>
194+
{page?.description && page?.title.length <= 64 ? (
195+
<h2 tw={tcls('text-4xl', 'mb-0', 'mt-8', 'w-[75%]', 'font-normal')}>
196+
{page.description.length > 164
197+
? page.description.slice(0, 164) + '...'
198+
: page.description}
199+
</h2>
200+
) : null}
45201
</div>
46202
</div>
47203
),
48204
{
49205
width: 1200,
50206
height: 630,
207+
fonts: [
208+
{
209+
name: 'Inter',
210+
data: interRegular,
211+
weight: 400,
212+
style: 'normal',
213+
},
214+
{
215+
name: 'Inter',
216+
data: interBold,
217+
weight: 700,
218+
style: 'normal',
219+
},
220+
],
51221
},
52222
);
53223
}

packages/gitbook/src/app/(site)/layout.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { CustomizationRootLayout } from '@/components/RootLayout';
2-
import { getSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
2+
import { getSiteLayoutData } from '@/lib/api';
33
import { getSiteContentPointer } from '@/lib/pointer';
44

55
/**

packages/gitbook/src/app/(space)/~gitbook/pdf/layout.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { SpaceIntegrationScript } from '@gitbook/api';
2+
13
import { CustomizationRootLayout } from '@/components/RootLayout';
2-
import { getSiteLayoutData, getSpaceLayoutData } from '@/lib/api';
4+
import { getSiteLayoutData, getSpaceCustomization } from '@/lib/api';
35

46
import { getSiteOrSpacePointerForPDF } from './pointer';
57

@@ -19,3 +21,18 @@ export default async function PDFRootLayout(props: { children: React.ReactNode }
1921
<CustomizationRootLayout customization={customization}>{children}</CustomizationRootLayout>
2022
);
2123
}
24+
25+
/**
26+
* Fetch all the layout data about a space at once.
27+
*/
28+
async function getSpaceLayoutData(spaceId: string) {
29+
const [customization, scripts] = await Promise.all([
30+
getSpaceCustomization(spaceId),
31+
[] as SpaceIntegrationScript[],
32+
]);
33+
34+
return {
35+
customization,
36+
scripts,
37+
};
38+
}

packages/gitbook/src/components/DocumentView/Block.tsx

+21-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import { BlockMath } from './Math';
3232
import { OpenAPI } from './OpenAPI';
3333
import { Paragraph } from './Paragraph';
3434
import { Quote } from './Quote';
35+
import { Stepper } from './Stepper';
36+
import { StepperStep } from './StepperStep';
3537
import { Table } from './Table';
3638
import { Tabs } from './Tabs';
3739

@@ -42,6 +44,13 @@ export interface BlockProps<Block extends DocumentBlock> extends DocumentContext
4244
style?: ClassValue;
4345
}
4446

47+
/**
48+
* Alternative to `assertNever` that returns `null` instead of throwing an error.
49+
*/
50+
function nullIfNever(value: never): null {
51+
return null;
52+
}
53+
4554
export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
4655
const { block, style, ...contextProps } = props;
4756

@@ -97,8 +106,14 @@ export function Block<T extends DocumentBlock>(props: BlockProps<T>) {
97106
return <IntegrationBlock {...props} {...contextProps} block={block} />;
98107
case 'synced-block':
99108
return <BlockSyncedBlock {...props} {...contextProps} block={block} />;
109+
case 'reusable-content':
110+
return null;
111+
case 'stepper':
112+
return <Stepper {...props} {...contextProps} block={block} />;
113+
case 'stepper-step':
114+
return <StepperStep {...props} {...contextProps} block={block} />;
100115
default:
101-
assertNever(block);
116+
return nullIfNever(block);
102117
}
103118
})();
104119

@@ -129,7 +144,6 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
129144
case 'code':
130145
case 'hint':
131146
case 'tabs':
132-
case 'synced-block':
133147
return <SkeletonParagraph id={id} style={style} />;
134148
case 'expandable':
135149
case 'table':
@@ -138,6 +152,9 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
138152
case 'divider':
139153
case 'content-ref':
140154
case 'integration':
155+
case 'stepper':
156+
case 'synced-block':
157+
case 'reusable-content':
141158
return <SkeletonCard id={id} style={style} />;
142159
case 'embed':
143160
case 'images':
@@ -146,8 +163,9 @@ function BlockPlaceholder(props: { block: DocumentBlock; style: ClassValue }) {
146163
case 'image':
147164
case 'code-line':
148165
case 'tabs-item':
166+
case 'stepper-step':
149167
throw new Error('Blocks should be directly rendered by parent');
150168
default:
151-
assertNever(block);
169+
return nullIfNever(block);
152170
}
153171
}

0 commit comments

Comments
 (0)