Compare commits

...

8 Commits

Author SHA1 Message Date
Zeno Kapitein 52a61fd1e3 Fix variant dropdown for sites without variants 2024-10-22 17:12:35 +02:00
Zeno Kapitein 5abe43b451 Better variable, add comments 2024-10-22 16:49:11 +02:00
Zeno Kapitein e82744d952 Formatting 2024-10-22 14:29:27 +02:00
Zeno Kapitein 68bfc636b2 Add changeset 2024-10-22 14:29:00 +02:00
Zeno Kapitein c008cc7cc2 Add support for sidebar-only layout 2024-10-22 14:16:08 +02:00
Zeno Kapitein a41fb53445 Make header buttons smaller 2024-10-22 14:00:37 +02:00
Zeno Kapitein 8f4085d392 Update tab styling and animation 2024-10-22 13:59:37 +02:00
Zeno Kapitein a4f81c28b0 Change position of variant selector for sites with sections 2024-10-22 12:58:49 +02:00
9 changed files with 101 additions and 65 deletions
+9
View File
@@ -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,