mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
445baaaa61
Co-authored-by: Zeno Kapitein <zenomilan@me.com>
241 lines
9.5 KiB
TypeScript
241 lines
9.5 KiB
TypeScript
import {
|
|
CustomizationCorners,
|
|
CustomizationHeaderPreset,
|
|
CustomizationIconsStyle,
|
|
type CustomizationSettings,
|
|
CustomizationSidebarBackgroundStyle,
|
|
CustomizationSidebarListStyle,
|
|
type CustomizationThemedColor,
|
|
type CustomizationTint,
|
|
type SiteCustomizationSettings,
|
|
} from '@gitbook/api';
|
|
import {
|
|
colorContrast,
|
|
colorScale,
|
|
type ColorScaleOptions,
|
|
DEFAULT_TINT_COLOR,
|
|
hexToRgb,
|
|
} from '@gitbook/colors';
|
|
import { IconsProvider, IconStyle } from '@gitbook/icons';
|
|
|
|
import { fontNotoColorEmoji, fonts, ibmPlexMono } from '@/fonts';
|
|
import { getSpaceLanguage } from '@/intl/server';
|
|
import { getStaticFileURL } from '@/lib/assets';
|
|
import { tcls } from '@/lib/tailwind';
|
|
|
|
import { ClientContexts } from './ClientContexts';
|
|
|
|
import '@gitbook/icons/style.css';
|
|
import './globals.css';
|
|
|
|
/**
|
|
* Layout shared between the content and the PDF renderer.
|
|
* It takes care of setting the theme and the language.
|
|
*/
|
|
export async function CustomizationRootLayout(props: {
|
|
customization: SiteCustomizationSettings | CustomizationSettings;
|
|
children: React.ReactNode;
|
|
}) {
|
|
const { customization, children } = props;
|
|
|
|
const language = getSpaceLanguage(customization);
|
|
const tintColor = getTintColor(customization);
|
|
const mixColor = getTintMixColor(customization.styling.primaryColor, tintColor);
|
|
const sidebarStyles = getSidebarStyles(customization);
|
|
|
|
return (
|
|
<html
|
|
suppressHydrationWarning
|
|
lang={customization.internationalization.locale}
|
|
className={tcls(
|
|
customization.header.preset === CustomizationHeaderPreset.None
|
|
? null
|
|
: 'scroll-pt-[76px]', // Take the sticky header in consideration for the scrolling
|
|
customization.styling.corners === CustomizationCorners.Straight
|
|
? ' straight-corners'
|
|
: '',
|
|
'theme' in customization.styling && `theme-${customization.styling.theme}`,
|
|
tintColor ? ' tint' : 'no-tint',
|
|
sidebarStyles.background && ` sidebar-${sidebarStyles.background}`,
|
|
sidebarStyles.list && ` sidebar-list-${sidebarStyles.list}`,
|
|
)}
|
|
>
|
|
<head>
|
|
{customization.privacyPolicy.url ? (
|
|
<link rel="privacy-policy" href={customization.privacyPolicy.url} />
|
|
) : null}
|
|
<style
|
|
nonce={
|
|
//Since I can't get the nonce to work for inline styles, we need to allow unsafe-inline
|
|
undefined
|
|
}
|
|
>{`
|
|
:root {
|
|
${generateColorVariable('primary', customization.styling.primaryColor.light)}
|
|
${generateColorVariable('tint', tintColor ? tintColor.light : DEFAULT_TINT_COLOR, { mix: mixColor && { color: mixColor.color.light, ratio: mixColor.ratio.light } })}
|
|
${generateColorVariable('neutral', DEFAULT_TINT_COLOR)}
|
|
|
|
--header-background: ${
|
|
/** If the site still has a (deprecated) custom header link or background set, we use that.
|
|
* These values are no longer supported in the Customiser, and will eventually be unsupported in the front-end. */
|
|
hexToRgb(
|
|
customization.header.backgroundColor?.light ??
|
|
tintColor?.light ??
|
|
customization.styling.primaryColor.light,
|
|
)
|
|
};
|
|
--header-link: ${hexToRgb(customization.header.linkColor?.light ?? colorContrast(tintColor?.light ?? customization.styling.primaryColor.light))};
|
|
}
|
|
|
|
.dark {
|
|
${generateColorVariable('primary', customization.styling.primaryColor.dark, { darkMode: true })}
|
|
${generateColorVariable('tint', tintColor ? tintColor.dark : DEFAULT_TINT_COLOR, { darkMode: true, mix: mixColor && { color: mixColor?.color.dark, ratio: mixColor.ratio.dark } })}
|
|
${generateColorVariable('neutral', DEFAULT_TINT_COLOR, { darkMode: true })}
|
|
|
|
--header-background: ${hexToRgb(customization.header.backgroundColor?.dark ?? tintColor?.dark ?? customization.styling.primaryColor.dark)};
|
|
--header-link: ${hexToRgb(customization.header.linkColor?.dark ?? colorContrast(tintColor?.dark ?? customization.styling.primaryColor.dark))};
|
|
}
|
|
`}</style>
|
|
</head>
|
|
<body
|
|
className={tcls(
|
|
fontNotoColorEmoji.className,
|
|
`${fonts[customization.styling.font].className}`,
|
|
`${ibmPlexMono.variable}`,
|
|
'bg-tint-base',
|
|
'theme-muted:bg-tint-subtle',
|
|
'theme-bold-tint:bg-tint-subtle',
|
|
|
|
'theme-gradient:bg-gradient-primary',
|
|
'theme-gradient-tint:bg-gradient-tint',
|
|
)}
|
|
>
|
|
<IconsProvider
|
|
assetsURL={process.env.GITBOOK_ICONS_URL ?? getStaticFileURL('icons')}
|
|
assetsURLToken={process.env.GITBOOK_ICONS_TOKEN}
|
|
assetsByStyles={{
|
|
'custom-icons': {
|
|
assetsURL: getStaticFileURL('icons'),
|
|
},
|
|
}}
|
|
iconStyle={
|
|
('icons' in customization.styling
|
|
? apiToIconsStyles[customization.styling.icons]
|
|
: null) || IconStyle.Regular
|
|
}
|
|
>
|
|
<ClientContexts language={language}>{children}</ClientContexts>
|
|
</IconsProvider>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the tint color from the customization settings.
|
|
* If the tint color is not set or it is a space customization, it will return the default color.
|
|
*/
|
|
function getTintColor(
|
|
customization: CustomizationSettings | SiteCustomizationSettings,
|
|
): CustomizationTint['color'] | undefined {
|
|
if ('tint' in customization.styling && customization.styling.tint) {
|
|
return {
|
|
light: customization.styling.tint?.color.light ?? DEFAULT_TINT_COLOR,
|
|
dark: customization.styling.tint?.color.dark ?? DEFAULT_TINT_COLOR,
|
|
};
|
|
}
|
|
}
|
|
|
|
function getTintMixColor(
|
|
primaryColor: CustomizationThemedColor,
|
|
tintColor: CustomizationTint['color'] | undefined,
|
|
): {
|
|
color: CustomizationThemedColor;
|
|
ratio: { light: number; dark: number };
|
|
} {
|
|
if (!tintColor) {
|
|
// Mix in a bit of the primary colour into neutral, to match with primary nicely.
|
|
return {
|
|
color: primaryColor,
|
|
ratio: {
|
|
light: 0.2,
|
|
dark: 0.1,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Mix in neutral into the tint colour to offset it from the primary, and to make the effect less intense.
|
|
// If the tint colour differs from the primary colour, we use the tint colour fully without mixing.
|
|
return {
|
|
color: {
|
|
light: DEFAULT_TINT_COLOR,
|
|
dark: DEFAULT_TINT_COLOR,
|
|
},
|
|
ratio: {
|
|
light: tintColor.light.toUpperCase() === primaryColor.light.toUpperCase() ? 0.4 : 0,
|
|
dark: tintColor.dark.toUpperCase() === primaryColor.dark.toUpperCase() ? 0.4 : 0,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the sidebar styles from the customization settings.
|
|
* If it is a space customization, it will return the default styles.
|
|
*/
|
|
function getSidebarStyles(
|
|
customization: CustomizationSettings | SiteCustomizationSettings,
|
|
): SiteCustomizationSettings['styling']['sidebar'] {
|
|
if ('sidebar' in customization.styling) {
|
|
return {
|
|
background: customization.styling.sidebar.background,
|
|
list: customization.styling.sidebar.list,
|
|
};
|
|
}
|
|
|
|
return {
|
|
background: CustomizationSidebarBackgroundStyle.Default,
|
|
list: CustomizationSidebarListStyle.Default,
|
|
};
|
|
}
|
|
|
|
type ColorInput = string;
|
|
function generateColorVariable(
|
|
name: string,
|
|
color: ColorInput | Record<string, string>,
|
|
{
|
|
withContrast = true,
|
|
...options // Pass any options along to the colorScale() function
|
|
}: ColorScaleOptions & {
|
|
withContrast?: boolean;
|
|
} = {},
|
|
) {
|
|
const shades: Record<string, string> =
|
|
typeof color === 'string'
|
|
? Object.fromEntries(
|
|
colorScale(color, options)
|
|
.map((shade, index) => [index + 1, shade])
|
|
.concat([['original', color]]),
|
|
)
|
|
: color;
|
|
|
|
return Object.entries(shades)
|
|
.map(([key, value]) => {
|
|
const rgbValue = hexToRgb(value); // Check the original hex value
|
|
const contrastValue = withContrast ? hexToRgb(colorContrast(value)) : undefined; // Add contrast if needed
|
|
return `--${name}-${key}: ${rgbValue}; ${
|
|
contrastValue ? `--contrast-${name}-${key}: ${contrastValue};` : ''
|
|
}`;
|
|
})
|
|
.join('\n');
|
|
}
|
|
|
|
const apiToIconsStyles: {
|
|
[key in CustomizationIconsStyle]: IconStyle;
|
|
} = {
|
|
[CustomizationIconsStyle.Regular]: IconStyle.Regular,
|
|
[CustomizationIconsStyle.Solid]: IconStyle.Solid,
|
|
[CustomizationIconsStyle.Duotone]: IconStyle.Duotone,
|
|
[CustomizationIconsStyle.Thin]: IconStyle.Thin,
|
|
[CustomizationIconsStyle.Light]: IconStyle.Light,
|
|
};
|