mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-10 07:16:03 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 52a61fd1e3 | |||
| 5abe43b451 | |||
| e82744d952 | |||
| 68bfc636b2 | |||
| c008cc7cc2 | |||
| a41fb53445 | |||
| 8f4085d392 | |||
| a4f81c28b0 |
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
'gitbook': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Update header styling of sections, variant selector, and button links
|
||||||
|
|
||||||
|
- Change position of variant selector depending on context (next to logo or in table of contents)
|
||||||
|
- Update section tab styling and animation
|
||||||
|
- Make header buttons smaller with a new `medium` button size
|
||||||
@@ -50,16 +50,12 @@ export function CompactHeader(props: {
|
|||||||
'grow-0',
|
'grow-0',
|
||||||
'md:grow',
|
'md:grow',
|
||||||
'sm:max-w-xs',
|
'sm:max-w-xs',
|
||||||
'lg:my-4',
|
'lg:mt-2',
|
||||||
|
'lg:mb-8',
|
||||||
'lg:max-w-full',
|
'lg:max-w-full',
|
||||||
'justify-self-end',
|
'justify-self-end',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isMultiVariants ? (
|
|
||||||
<div className={tcls('mb-2')}>
|
|
||||||
<SpacesDropdown space={space} spaces={spaces} buttonKind="bordered" />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<SearchButton>
|
<SearchButton>
|
||||||
<span className={tcls('flex-1')}>
|
<span className={tcls('flex-1')}>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { CustomizationSettings, Site, SiteCustomizationSettings, Space } from '@gitbook/api';
|
import {
|
||||||
|
CustomizationSettings,
|
||||||
|
Site,
|
||||||
|
SiteCustomizationSettings,
|
||||||
|
SiteSection,
|
||||||
|
Space,
|
||||||
|
} from '@gitbook/api';
|
||||||
import { CustomizationHeaderPreset } from '@gitbook/api';
|
import { CustomizationHeaderPreset } from '@gitbook/api';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
@@ -13,6 +19,7 @@ import { HeaderLinks } from './HeaderLinks';
|
|||||||
import { HeaderLogo } from './HeaderLogo';
|
import { HeaderLogo } from './HeaderLogo';
|
||||||
import { SpacesDropdown } from './SpacesDropdown';
|
import { SpacesDropdown } from './SpacesDropdown';
|
||||||
import { SearchButton } from '../Search';
|
import { SearchButton } from '../Search';
|
||||||
|
import { SiteSectionTabs } from '../SiteSectionTabs';
|
||||||
/**
|
/**
|
||||||
* Render the header for the space.
|
* Render the header for the space.
|
||||||
*/
|
*/
|
||||||
@@ -20,14 +27,17 @@ export function Header(props: {
|
|||||||
space: Space;
|
space: Space;
|
||||||
site: Site | null;
|
site: Site | null;
|
||||||
spaces: Space[];
|
spaces: Space[];
|
||||||
|
sections: { list: SiteSection[]; section: SiteSection } | null;
|
||||||
context: ContentRefContext;
|
context: ContentRefContext;
|
||||||
customization: CustomizationSettings | SiteCustomizationSettings;
|
customization: CustomizationSettings | SiteCustomizationSettings;
|
||||||
withTopHeader?: boolean;
|
withTopHeader?: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { children, context, space, site, spaces, customization, withTopHeader } = props;
|
const { children, context, space, site, spaces, sections, customization, withTopHeader } =
|
||||||
|
props;
|
||||||
const isCustomizationDefault =
|
const isCustomizationDefault =
|
||||||
customization.header.preset === CustomizationHeaderPreset.Default;
|
customization.header.preset === CustomizationHeaderPreset.Default;
|
||||||
|
const hasSiteSections = sections && sections.list.length > 1;
|
||||||
const isMultiVariants = site && spaces.length > 1;
|
const isMultiVariants = site && spaces.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -70,7 +80,9 @@ export function Header(props: {
|
|||||||
>
|
>
|
||||||
<HeaderLogo site={site} space={space} customization={customization} />
|
<HeaderLogo site={site} space={space} customization={customization} />
|
||||||
<span>
|
<span>
|
||||||
{isMultiVariants ? <SpacesDropdown space={space} spaces={spaces} /> : null}
|
{!hasSiteSections && isMultiVariants ? (
|
||||||
|
<SpacesDropdown space={space} spaces={spaces} />
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<HeaderLinks>
|
<HeaderLinks>
|
||||||
{customization.header.links.map((link, index) => {
|
{customization.header.links.map((link, index) => {
|
||||||
@@ -134,7 +146,17 @@ export function Header(props: {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{sections ? (
|
||||||
|
<div
|
||||||
|
className={tcls(
|
||||||
|
'w-full shadow-thintop dark:shadow-light/1 bg-light dark:bg-dark z-[9] mt-0.5',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={tcls(CONTAINER_STYLE)}>
|
||||||
|
<SiteSectionTabs sections={sections.list} section={sections.section} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export async function HeaderLink(props: {
|
|||||||
<Button
|
<Button
|
||||||
href={target.href}
|
href={target.href}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
size="medium"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
{
|
{
|
||||||
'button-primary':
|
'button-primary':
|
||||||
|
|||||||
@@ -5,45 +5,37 @@ import { tcls } from '@/lib/tailwind';
|
|||||||
import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
|
import { Dropdown, DropdownChevron, DropdownMenu } from './Dropdown';
|
||||||
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
import { SpacesDropdownMenuItem } from './SpacesDropdownMenuItem';
|
||||||
|
|
||||||
export function SpacesDropdown(props: {
|
export function SpacesDropdown(props: { space: Space; spaces: Space[] }) {
|
||||||
space: Space;
|
const { space, spaces } = props;
|
||||||
spaces: Space[];
|
|
||||||
buttonKind?: 'default' | 'bordered';
|
|
||||||
}) {
|
|
||||||
const { space, spaces, buttonKind = 'default' } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className={buttonKind === 'bordered' ? tcls('w-full') : undefined}
|
|
||||||
button={(buttonProps) => (
|
button={(buttonProps) => (
|
||||||
<div
|
<div
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
data-testid="space-dropdown-button"
|
data-testid="space-dropdown-button"
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'justify-self-start',
|
|
||||||
'flex',
|
'flex',
|
||||||
'flex-row',
|
'flex-row',
|
||||||
'items-center',
|
'items-center',
|
||||||
|
'rounded-2xl',
|
||||||
|
'straight-corners:rounded-none',
|
||||||
|
'bg-light-2',
|
||||||
|
'border',
|
||||||
|
'border-light-3',
|
||||||
|
'text-dark-4',
|
||||||
|
'text-sm',
|
||||||
'px-3',
|
'px-3',
|
||||||
'py-1.5',
|
'py-1',
|
||||||
'text-header-link-500',
|
'contrast-more:border-dark',
|
||||||
buttonKind === 'bordered'
|
'contrast-more:bg-light',
|
||||||
? [
|
'contrast-more:text-dark',
|
||||||
'ring-1',
|
'dark:bg-dark-3',
|
||||||
'ring-inset',
|
'dark:border-dark-4',
|
||||||
'ring-dark/2',
|
'dark:text-light-4',
|
||||||
'pointer-events-auto',
|
'contrast-more:dark:border-light',
|
||||||
'justify-between',
|
'contrast-more:dark:bg-dark',
|
||||||
'bg-light',
|
'contrast-more:dark:text-light',
|
||||||
'dark:bg-dark',
|
|
||||||
'rounded-lg',
|
|
||||||
'straight-corners:rounded-none',
|
|
||||||
'lg:ring-0',
|
|
||||||
'border',
|
|
||||||
'border-dark/2',
|
|
||||||
'dark:border-light/2',
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{space.title}
|
{space.title}
|
||||||
|
|||||||
@@ -69,9 +69,24 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'relative flex gap-2 bg-transparent',
|
'relative',
|
||||||
|
'flex',
|
||||||
|
'gap-2',
|
||||||
|
'bg-transparent',
|
||||||
/* add a pseudo element for active tab indicator */
|
/* add a pseudo element for active tab indicator */
|
||||||
"after:block after:content-[''] after:origin-left after:absolute after:-bottom-px after:left-0 after:scale-x-[--tab-scale] after:transition-transform after:translate-x-[var(--tab-start)] after:h-0.5 after:w-[100px] after:bg-primary dark:after:bg-primary-500",
|
'after:block',
|
||||||
|
"after:content-['']",
|
||||||
|
'after:origin-left',
|
||||||
|
'after:absolute',
|
||||||
|
'after:-bottom-px',
|
||||||
|
'after:left-0',
|
||||||
|
'after:scale-x-[--tab-scale]',
|
||||||
|
'after:transition-transform',
|
||||||
|
'after:translate-x-[var(--tab-start)]',
|
||||||
|
'after:h-0.5',
|
||||||
|
'after:w-[100px]',
|
||||||
|
'after:bg-primary',
|
||||||
|
'dark:after:bg-primary-400',
|
||||||
)}
|
)}
|
||||||
role="tablist"
|
role="tablist"
|
||||||
>
|
>
|
||||||
@@ -82,6 +97,7 @@ export function SiteSectionTabs(props: { sections: SiteSection[]; section: SiteS
|
|||||||
label={tab.label}
|
label={tab.label}
|
||||||
href={tab.path}
|
href={tab.path}
|
||||||
ref={currentIndex === index ? currentTabRef : null}
|
ref={currentIndex === index ? currentTabRef : null}
|
||||||
|
onClick={() => setCurrentIndex(index)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -101,9 +117,9 @@ const Tab = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={tcls(
|
className={tcls(
|
||||||
'px-3 py-3.5 rounded',
|
'px-3 py-1 my-2 rounded straight-corners:rounded-none transition-colors',
|
||||||
active && 'text-primary dark:text-primary-500',
|
active && 'text-primary dark:text-primary-400',
|
||||||
!active && 'hover:text-primary dark:hover:text-primary-500 transition-colors',
|
!active && ' hover:bg-light-2 dark:hover:bg-dark-3',
|
||||||
)}
|
)}
|
||||||
role="tab"
|
role="tab"
|
||||||
href={href}
|
href={href}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { ContentTarget, SiteContentPointer } from '@/lib/api';
|
|||||||
import { ContentRefContext } from '@/lib/references';
|
import { ContentRefContext } from '@/lib/references';
|
||||||
import { tcls } from '@/lib/tailwind';
|
import { tcls } from '@/lib/tailwind';
|
||||||
|
|
||||||
import { SiteSectionTabs } from '../SiteSectionTabs';
|
import { SpacesDropdown } from '../Header/SpacesDropdown';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the entire content of the space (header, table of contents, footer, and page content).
|
* Render the entire content of the space (header, table of contents, footer, and page content).
|
||||||
@@ -61,6 +61,7 @@ export function SpaceLayout(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const withSections = Boolean(sections && sections.list.length > 0);
|
const withSections = Boolean(sections && sections.list.length > 0);
|
||||||
|
const withVariants = Boolean(site && spaces.length > 1);
|
||||||
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
|
const headerOffset = { sectionsHeader: withSections, topHeader: withTopHeader };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -71,21 +72,10 @@ export function SpaceLayout(props: {
|
|||||||
space={space}
|
space={space}
|
||||||
site={site}
|
site={site}
|
||||||
spaces={spaces}
|
spaces={spaces}
|
||||||
|
sections={sections}
|
||||||
context={contentRefContext}
|
context={contentRefContext}
|
||||||
customization={customization}
|
customization={customization}
|
||||||
>
|
/>
|
||||||
{sections ? (
|
|
||||||
<div
|
|
||||||
className={tcls(
|
|
||||||
'w-full shadow-thintop dark:shadow-light/1 bg-light dark:bg-dark z-[9] mt-0.5',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={tcls(CONTAINER_STYLE)}>
|
|
||||||
<SiteSectionTabs sections={sections.list} section={sections.section} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</Header>
|
|
||||||
<div className={tcls('scroll-nojump')}>
|
<div className={tcls('scroll-nojump')}>
|
||||||
<div
|
<div
|
||||||
className={tcls(
|
className={tcls(
|
||||||
@@ -116,6 +106,11 @@ export function SpaceLayout(props: {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
innerHeader={
|
||||||
|
(sections || !withTopHeader) && withVariants ? (
|
||||||
|
<SpacesDropdown space={space} spaces={spaces} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
headerOffset={headerOffset}
|
headerOffset={headerOffset}
|
||||||
/>
|
/>
|
||||||
<div className={tcls('flex-1', 'flex', 'flex-col')}>{children}</div>
|
<div className={tcls('flex-1', 'flex', 'flex-col')}>{children}</div>
|
||||||
|
|||||||
@@ -33,10 +33,12 @@ export function TableOfContents(props: {
|
|||||||
context: ContentRefContext;
|
context: ContentRefContext;
|
||||||
pages: Revision['pages'];
|
pages: Revision['pages'];
|
||||||
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
ancestors: Array<RevisionPageDocument | RevisionPageGroup>;
|
||||||
header?: React.ReactNode;
|
header?: React.ReactNode; // Displayed outside the scrollable TOC as a sticky header
|
||||||
headerOffset: { sectionsHeader: boolean; topHeader: boolean };
|
headerOffset: { sectionsHeader: boolean; topHeader: boolean };
|
||||||
|
innerHeader?: React.ReactNode; // Displayed inside the scrollable TOC, directly above the page list
|
||||||
}) {
|
}) {
|
||||||
const { space, customization, pages, ancestors, header, context, headerOffset } = props;
|
const { innerHeader, space, customization, pages, ancestors, header, context, headerOffset } =
|
||||||
|
props;
|
||||||
|
|
||||||
const withHeaderOffset = headerOffset.sectionsHeader || headerOffset.topHeader;
|
const withHeaderOffset = headerOffset.sectionsHeader || headerOffset.topHeader;
|
||||||
const topOffset = getTopOffset(headerOffset);
|
const topOffset = getTopOffset(headerOffset);
|
||||||
@@ -91,6 +93,7 @@ export function TableOfContents(props: {
|
|||||||
customization.trademark.enabled ? 'lg:pb-20' : 'lg:pb-4',
|
customization.trademark.enabled ? 'lg:pb-20' : 'lg:pb-4',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{innerHeader && <div className={tcls('ms-5', 'mb-4')}>{innerHeader}</div>}
|
||||||
<PagesList
|
<PagesList
|
||||||
rootPages={pages}
|
rootPages={pages}
|
||||||
pages={pages}
|
pages={pages}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type ButtonProps = {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: 'primary' | 'secondary';
|
variant?: 'primary' | 'secondary';
|
||||||
size?: 'default' | 'small';
|
size?: 'default' | 'medium' | 'small';
|
||||||
className?: ClassValue;
|
className?: ClassValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,12 +43,13 @@ export function Button({
|
|||||||
'dark:hover:bg-light/3',
|
'dark:hover:bg-light/3',
|
||||||
];
|
];
|
||||||
|
|
||||||
const sizeClasses =
|
const sizes = {
|
||||||
size === 'default'
|
default: ['text-base', 'px-4', 'py-2'],
|
||||||
? // DEFAULT
|
medium: ['text-base', 'px-3', 'py-1'],
|
||||||
['text-base', 'px-4', 'py-2']
|
small: ['text-xs', 'px-3 py-2'],
|
||||||
: // SMALL
|
};
|
||||||
['text-xs', 'px-3 py-2'];
|
|
||||||
|
const sizeClasses = sizes[size] || sizes.default;
|
||||||
|
|
||||||
const domClassName = tcls(
|
const domClassName = tcls(
|
||||||
'inline-block',
|
'inline-block',
|
||||||
@@ -60,6 +61,7 @@ export function Button({
|
|||||||
'grow-0',
|
'grow-0',
|
||||||
'shrink-0',
|
'shrink-0',
|
||||||
'truncate',
|
'truncate',
|
||||||
|
'transition-colors',
|
||||||
variantClasses,
|
variantClasses,
|
||||||
sizeClasses,
|
sizeClasses,
|
||||||
className,
|
className,
|
||||||
|
|||||||
Reference in New Issue
Block a user