Refactor StructurePreview and HeaderLogo components to utilize HeaderLogoContent for improved logo rendering and fallback handling

This commit is contained in:
Nicolas Dorseuil
2026-06-16 11:45:02 +02:00
parent 52fc156b06
commit 8a5ed60c26
3 changed files with 171 additions and 133 deletions
@@ -1,11 +1,16 @@
import type { GitBookSiteContext } from '@/lib/context';
import { Image } from '@/components/utils';
import { tcls } from '@/lib/tailwind';
import { resolveContentRef } from '@/lib/references';
import { Link } from '../primitives';
import { CurrentContentIcon } from './CurrentContentIcon';
import {
HEADER_LOGO_CONTAINER_CLASS,
HEADER_LOGO_IMAGE_CLASS,
HEADER_LOGO_IMAGE_SIZES,
HeaderLogoContent,
} from './HeaderLogoContent';
interface HeaderLogoProps {
context: GitBookSiteContext;
@@ -26,83 +31,56 @@ export async function HeaderLogo(props: HeaderLogoProps) {
return (
<Link
href={primaryLink?.href ?? linker.toPathInSite('')}
className={tcls('group/headerlogo', 'min-w-0', 'shrink', 'flex', 'items-center')}
className={HEADER_LOGO_CONTAINER_CLASS}
>
{customization.header.logo ? (
<Image
alt="Logo"
resize={context.imageResizer}
sources={{
light: {
src: customization.header.logo.light,
},
dark: customization.header.logo.dark
? {
src: customization.header.logo.dark,
}
: null,
}}
sizes={[
{
media: '(max-width: 1024px)',
width: 160,
},
{
width: 260,
},
]}
preload
style={tcls(
'overflow-hidden',
'shrink',
'min-w-0',
'max-w-40',
'lg:max-w-64',
'lg:site-header-none:page-no-toc:max-w-56',
'max-h-8',
'h-full',
'w-full',
'object-contain',
'object-left'
)}
/>
) : (
<LogoFallback {...props} />
)}
<HeaderLogoContent
logo={customization.header.logo ? <LogoImage context={context} /> : null}
fallbackIcon={<LogoFallbackIcon context={context} />}
title={context.site.title}
/>
</Link>
);
}
function LogoFallback(props: HeaderLogoProps) {
function LogoImage(props: HeaderLogoProps) {
const { context } = props;
const { site } = context;
const { customization } = context;
if (!customization.header.logo) {
return null;
}
return (
<>
<CurrentContentIcon
context={context}
alt=""
sizes={[{ width: 32 }]}
style={['object-contain', 'size-8']}
fetchPriority="high"
/>
<div
className={tcls(
'text-pretty',
'line-clamp-2',
'tracking-tight',
'max-w-[18ch]',
'lg:max-w-[24ch]',
'font-semibold',
'ms-3',
'text-base/tight',
'lg:text-lg/tight',
'text-tint-strong',
'theme-bold:text-header-link'
)}
>
{site.title}
</div>
</>
<Image
alt="Logo"
resize={context.imageResizer}
sources={{
light: {
src: customization.header.logo.light,
},
dark: customization.header.logo.dark
? {
src: customization.header.logo.dark,
}
: null,
}}
sizes={HEADER_LOGO_IMAGE_SIZES}
preload
style={HEADER_LOGO_IMAGE_CLASS}
/>
);
}
function LogoFallbackIcon(props: HeaderLogoProps) {
const { context } = props;
return (
<CurrentContentIcon
context={context}
alt=""
sizes={[{ width: 32 }]}
style={['object-contain', 'size-8']}
fetchPriority="high"
/>
);
}
@@ -0,0 +1,78 @@
import type { ReactNode } from 'react';
import { tcls } from '@/lib/tailwind';
export const HEADER_LOGO_IMAGE_SIZES = [
{
media: '(max-width: 1024px)',
width: 160,
},
{
width: 260,
},
];
export const HEADER_LOGO_CONTAINER_CLASS = tcls(
'group/headerlogo',
'min-w-0',
'shrink',
'flex',
'items-center'
);
export const HEADER_LOGO_IMAGE_CLASS = tcls(
'overflow-hidden',
'shrink',
'min-w-0',
'max-w-40',
'lg:max-w-64',
'lg:site-header-none:page-no-toc:max-w-56',
'max-h-8',
'h-full',
'w-full',
'object-contain',
'object-left'
);
interface HeaderLogoContentProps {
logo: ReactNode | null;
fallbackIcon: ReactNode;
title: ReactNode;
}
export function HeaderLogoContent(props: HeaderLogoContentProps) {
const { logo, fallbackIcon, title } = props;
if (logo) {
return logo;
}
return (
<>
{fallbackIcon}
<HeaderLogoTitle>{title}</HeaderLogoTitle>
</>
);
}
function HeaderLogoTitle(props: { children: ReactNode }) {
return (
<div
className={tcls(
'text-pretty',
'line-clamp-2',
'tracking-tight',
'max-w-[18ch]',
'lg:max-w-[24ch]',
'font-semibold',
'ms-3',
'text-base/tight',
'lg:text-lg/tight',
'text-tint-strong',
'theme-bold:text-header-link'
)}
>
{props.children}
</div>
);
}
@@ -10,6 +10,11 @@ import { getLocalizedTitle } from '@/lib/sites';
import { tcls } from '@/lib/tailwind';
import { tString, useLanguage } from '@/intl/client';
import {
HEADER_LOGO_CONTAINER_CLASS,
HEADER_LOGO_IMAGE_CLASS,
HeaderLogoContent,
} from '../Header/HeaderLogoContent';
import headerLinksStyles from '../Header/headerLinks.module.css';
import { CONTAINER_STYLE, HEADER_HEIGHT_DESKTOP } from '../layout';
import { Button, ToggleChevron } from '../primitives';
@@ -255,72 +260,49 @@ function StructurePreviewLogo(props: { snapshot: StructurePreviewSnapshot }) {
const { customization } = snapshot;
return (
<div className={tcls('group/headerlogo', 'min-w-0', 'shrink', 'flex', 'items-center')}>
{customization.header.logo ? (
<picture>
{customization.header.logo.dark ? (
<source
srcSet={customization.header.logo.dark}
media="(prefers-color-scheme: dark)"
/>
) : null}
<img
alt="Logo"
src={customization.header.logo.light}
className={tcls(
'block overflow-hidden',
'shrink',
'min-w-0',
'max-w-40',
'lg:max-w-64',
'max-h-8',
'h-full',
'w-full',
'object-contain',
'object-left'
)}
/>
</picture>
) : (
<>
{'emoji' in customization.favicon && customization.favicon.emoji ? (
<span className="text-xl">{customization.favicon.emoji}</span>
) : (
<picture>
<source
srcSet={snapshot.icons.large.dark}
media="(prefers-color-scheme: dark)"
/>
<img
alt=""
src={snapshot.icons.large.light}
className="size-8 object-contain"
/>
</picture>
)}
<div
className={tcls(
'text-pretty',
'line-clamp-2',
'tracking-tight',
'max-w-[18ch]',
'lg:max-w-[24ch]',
'font-semibold',
'ms-3',
'text-base/tight',
'lg:text-lg/tight',
'text-tint-strong',
'theme-bold:text-header-link'
)}
>
{snapshot.site.title}
</div>
</>
)}
<div className={HEADER_LOGO_CONTAINER_CLASS}>
<HeaderLogoContent
logo={
customization.header.logo ? (
<StructurePreviewLogoImage logo={customization.header.logo} />
) : null
}
fallbackIcon={<StructurePreviewLogoFallbackIcon snapshot={snapshot} />}
title={snapshot.site.title}
/>
</div>
);
}
function StructurePreviewLogoImage(props: {
logo: NonNullable<StructurePreviewSnapshot['customization']['header']['logo']>;
}) {
const { logo } = props;
return (
<picture>
{logo.dark ? <source srcSet={logo.dark} media="(prefers-color-scheme: dark)" /> : null}
<img alt="Logo" src={logo.light} className={tcls('block', HEADER_LOGO_IMAGE_CLASS)} />
</picture>
);
}
function StructurePreviewLogoFallbackIcon(props: { snapshot: StructurePreviewSnapshot }) {
const { snapshot } = props;
const { customization } = snapshot;
if ('emoji' in customization.favicon && customization.favicon.emoji) {
return <span className="text-xl">{customization.favicon.emoji}</span>;
}
return (
<picture>
<source srcSet={snapshot.icons.large.dark} media="(prefers-color-scheme: dark)" />
<img alt="" src={snapshot.icons.large.light} className="size-8 object-contain" />
</picture>
);
}
function StructurePreviewSearch(props: { style: CustomizationSearchStyle }) {
const language = useLanguage();
const label = tString(language, 'search');