mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-16 15:45:13 +00:00
Restyle footer, allow >4 link groups (#2761)
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'gitbook': patch
|
||||
---
|
||||
|
||||
Update footer styling and allow for more than 4 footer groups
|
||||
@@ -2,6 +2,7 @@ import { CustomizationSettings, SiteCustomizationSettings, Space } from '@gitboo
|
||||
import React from 'react';
|
||||
|
||||
import { Image } from '@/components/utils';
|
||||
import { partition } from '@/lib/arrays';
|
||||
import { ContentRefContext } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
@@ -9,6 +10,8 @@ import { FooterLinksGroup } from './FooterLinksGroup';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { ThemeToggler } from '../ThemeToggler';
|
||||
|
||||
const FOOTER_COLUMNS = 4;
|
||||
|
||||
export function Footer(props: {
|
||||
space: Space;
|
||||
context: ContentRefContext;
|
||||
@@ -17,125 +20,115 @@ export function Footer(props: {
|
||||
const { context, customization } = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={tcls(
|
||||
'border-t',
|
||||
'border-dark/2',
|
||||
'bg-light-2',
|
||||
'dark:border-light/2',
|
||||
'dark:bg-dark-2',
|
||||
)}
|
||||
>
|
||||
<div className={tcls('scroll-nojump')}>
|
||||
<footer className="border-t border-dark/2 dark:border-light/2 scroll-nojump">
|
||||
<div
|
||||
className={tcls(
|
||||
CONTAINER_STYLE,
|
||||
'py-8',
|
||||
'gap-12',
|
||||
'flex',
|
||||
'flex-wrap',
|
||||
'items-start',
|
||||
)}
|
||||
>
|
||||
{/* Footer Logo */}
|
||||
<div className="shrink sm:basis-72 page-no-toc:lg:basis-56 mr-auto order-1 empty:hidden empty:lg:block page-no-toc:empty:lg:hidden page-no-toc:empty:xl:block">
|
||||
{customization.footer.logo ? (
|
||||
<Image
|
||||
alt="Logo"
|
||||
sources={{
|
||||
light: {
|
||||
src: customization.footer.logo.light,
|
||||
},
|
||||
dark: customization.footer.logo.dark
|
||||
? {
|
||||
src: customization.footer.logo.dark,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority="lazy"
|
||||
style={[
|
||||
'w-auto',
|
||||
'max-w-40',
|
||||
'lg:max-w-64',
|
||||
'max-h-12',
|
||||
'object-contain',
|
||||
'object-left',
|
||||
'rounded',
|
||||
'straight-corners:rounded-sm',
|
||||
]}
|
||||
sizes={[
|
||||
{
|
||||
width: 320,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Mode Switcher */}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
CONTAINER_STYLE,
|
||||
|
||||
'py-6',
|
||||
'gap-6',
|
||||
'md:flex-row',
|
||||
'md:gap-10',
|
||||
|
||||
//override CONTAINER_STYLE px
|
||||
'px-0',
|
||||
'sm:px-0',
|
||||
'ml-auto',
|
||||
'order-2 lg:order-4',
|
||||
customization.footer.groups?.length < 3 &&
|
||||
customization.footer.logo == undefined &&
|
||||
'sm:order-4',
|
||||
'xl:basis-56',
|
||||
)}
|
||||
>
|
||||
{' '}
|
||||
{customization.footer.logo ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'pt-8',
|
||||
'border-t',
|
||||
'border-dark/2',
|
||||
'dark:border-light/1',
|
||||
'px-4',
|
||||
'sm:px-6',
|
||||
'md:px-0',
|
||||
'md:pr-10',
|
||||
'md:border-r',
|
||||
'md:border-t-0',
|
||||
'md:pt-0',
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
alt="Logo"
|
||||
sources={{
|
||||
light: {
|
||||
src: customization.footer.logo.light,
|
||||
},
|
||||
dark: customization.footer.logo.dark
|
||||
? {
|
||||
src: customization.footer.logo.dark,
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
priority="lazy"
|
||||
style={['h-auto', 'w-full', 'max-w-12', 'object-contain']}
|
||||
sizes={[
|
||||
{
|
||||
width: 320,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={tcls(
|
||||
'flex-1',
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-6',
|
||||
'px-4',
|
||||
'sm:px-6',
|
||||
'md:px-0',
|
||||
)}
|
||||
>
|
||||
{customization.footer.logo || customization.footer.groups?.length > 0 ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'gap-10',
|
||||
'sm:gap-20',
|
||||
'sm:flex-row',
|
||||
'items-start',
|
||||
'flex-wrap',
|
||||
)}
|
||||
>
|
||||
{customization.footer.groups.map((group, index) => (
|
||||
<FooterLinksGroup key={index} group={group} context={context} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{customization.footer.copyright ? (
|
||||
<p className={tcls('text-xs', 'text-dark/7', 'dark:text-light/6')}>
|
||||
{customization.footer.copyright}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
{customization.themes.toggeable ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-col',
|
||||
'items-start',
|
||||
'order-[-1]',
|
||||
'px-4',
|
||||
'sm:px-6',
|
||||
'md:px-0',
|
||||
'md:order-1',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-end">
|
||||
<React.Suspense fallback={null}>
|
||||
<ThemeToggler />
|
||||
</React.Suspense>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Navigation Groups (split into equal columns) */}
|
||||
{customization.footer.groups?.length > 0 ? (
|
||||
<div
|
||||
className={tcls(
|
||||
'flex flex-col sm:flex-row mx-auto grow lg:max-w-3xl gap-10 sm:gap-6 order-3',
|
||||
'w-full lg:w-auto',
|
||||
customization.footer.groups?.length < 3 &&
|
||||
customization.footer.logo == undefined &&
|
||||
'sm:w-auto',
|
||||
)}
|
||||
>
|
||||
{partition(customization.footer.groups, FOOTER_COLUMNS).map(
|
||||
(column, columnIndex) => (
|
||||
<div key={columnIndex} className="flex w-full flex-col gap-10">
|
||||
{column.map((group, groupIndex) => (
|
||||
<FooterLinksGroup
|
||||
key={groupIndex}
|
||||
group={group}
|
||||
context={context}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Legal */}
|
||||
<div
|
||||
className={tcls(
|
||||
'mx-auto w-full grow text-xs text-dark/7 dark:text-light/6 items-center text-center order-4 flex flex-col gap-2 empty:hidden',
|
||||
customization.footer.groups.length == 0 &&
|
||||
'order-2 lg:flex-1 lg:w-auto lg:items-start lg:max-w-3xl self-center lg:text-start',
|
||||
customization.footer.groups.length == 0 &&
|
||||
customization.footer.logo == undefined &&
|
||||
'sm:w-auto sm:flex-1 sm:items-start sm:max-w-3xl sm:text-start',
|
||||
)}
|
||||
>
|
||||
{customization.footer.copyright ? (
|
||||
<p>{customization.footer.copyright}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,12 +16,18 @@ export function FooterLinksGroup(props: {
|
||||
const { group, context } = props;
|
||||
|
||||
return (
|
||||
<div className={tcls('flex', 'flex-col', 'gap-3')}>
|
||||
<p className={tcls('text-base', 'font-semibold')}>{group.title}</p>
|
||||
{group.links.map((link, index) => {
|
||||
return <FooterLink key={index} link={link} context={context} />;
|
||||
})}
|
||||
</div>
|
||||
<nav className="text-sm flex flex-col gap-4">
|
||||
<h4 className="font-semibold">{group.title}</h4>
|
||||
<ul className="flex flex-col items-start gap-4">
|
||||
{group.links.map((link, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
<FooterLink link={link} context={context} />
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,12 +43,15 @@ async function FooterLink(props: { link: CustomizationContentLink; context: Cont
|
||||
<Link
|
||||
href={resolved.href}
|
||||
className={tcls(
|
||||
'text-sm',
|
||||
'font-normal',
|
||||
'text-dark/8',
|
||||
'hover:text-dark/9',
|
||||
'hover:text-primary',
|
||||
'dark:text-light/8',
|
||||
'dark:hover:text-light/9',
|
||||
'dark:hover:text-light',
|
||||
'contrast-more:underline',
|
||||
'contrast-more:text-dark',
|
||||
'contrast-more:dark:text-light',
|
||||
'underline-offset-2',
|
||||
)}
|
||||
insights={{
|
||||
target: link.to,
|
||||
|
||||
@@ -27,18 +27,7 @@ export function ThemeToggler(props: {}) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
className={tcls(
|
||||
'flex',
|
||||
'flex-row',
|
||||
'rounded-full',
|
||||
'straight-corners:rounded-sm',
|
||||
'border',
|
||||
'border-dark/3',
|
||||
'dark:border-light/2',
|
||||
)}
|
||||
>
|
||||
<div role="radiogroup" className="flex flex-row gap-2">
|
||||
<ThemeButton
|
||||
active={mounted && theme === 'light'}
|
||||
icon="sun-bright"
|
||||
@@ -74,25 +63,37 @@ function ThemeButton(props: {
|
||||
role="radio"
|
||||
onClick={onClick}
|
||||
aria-label={title}
|
||||
title={title}
|
||||
aria-checked={active}
|
||||
className={tcls(
|
||||
'p-1',
|
||||
'm-1',
|
||||
'group',
|
||||
'rounded-full',
|
||||
'straight-corners:rounded-sm',
|
||||
active ? ['bg-primary-600/4', 'dark:bg-primary-400/2'] : null,
|
||||
'text-dark',
|
||||
'dark:text-light/7',
|
||||
'p-2',
|
||||
'rounded',
|
||||
'straight-corners:rounded-none',
|
||||
'transition-all',
|
||||
'text-dark/8',
|
||||
'dark:text-light/8',
|
||||
'contrast-more:text-dark',
|
||||
'dark:contrast-more:text-light',
|
||||
'hover:bg-dark/1',
|
||||
'dark:hover:bg-light/1',
|
||||
'contrast-more:hover:ring-2',
|
||||
'contrast-more:focus:ring-2',
|
||||
'ring-dark',
|
||||
'dark:ring-light',
|
||||
active && [
|
||||
'bg-tint/2',
|
||||
'hover:bg-tint/2',
|
||||
'text-tint-600',
|
||||
'dark:text-tint-400',
|
||||
'contrast-more:text-tint-600',
|
||||
'contrast-more:dark:text-tint-400',
|
||||
'contrast-more:ring-1',
|
||||
'ring-tint-600',
|
||||
'dark:ring-tint-400',
|
||||
],
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
className={tcls(
|
||||
'size-4',
|
||||
active ? ['text-primary-600', 'dark:text-primary-400'] : null,
|
||||
)}
|
||||
/>
|
||||
<Icon icon={icon} className={tcls('size-4')} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Partition an array into equal (+ rest items) parts.
|
||||
* Adapted from https://stackoverflow.com/a/68917937
|
||||
*/
|
||||
export function partition(
|
||||
/**
|
||||
* Array to split up
|
||||
*/
|
||||
array: any[],
|
||||
/**
|
||||
* Amount of parts you want to split into
|
||||
*/
|
||||
parts: number,
|
||||
) {
|
||||
let rest = array.length % parts;
|
||||
let size = Math.floor(array.length / parts);
|
||||
let j = 0;
|
||||
|
||||
return Array.from({ length: Math.min(array.length, parts) }, (_, i) =>
|
||||
array.slice(j, (j += size + (i < rest ? 1 : 0))),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user