1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-15 01:16:22 +02:00

docs: new theme footer (#9158)

https://linear.app/unleash/issue/2-3177/footer

New docs theme footer.

Kept [swizzling](https://docusaurus.io/docs/swizzling) to a minimum and
instead tried to keep most things in CSS to avoid adding more
complexity.

Remember to test responsiveness by resizing your window, as well as dark
mode.

[Preview
link](https://unleash-docs-git-docs-new-theme-footer-unleash-team.vercel.app/)
This commit is contained in:
Nuno Góis 2025-01-28 16:22:24 +00:00 committed by GitHub
parent 18857c8992
commit b62c1d6c1e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 715 additions and 21 deletions

View File

@ -52,7 +52,7 @@ const getUnleashRepoStars = async () => {
const formattedStars =
unleashRepoStars >= 1000
? `${(unleashRepoStars / 1000).toFixed(1)}k`
: unleashRepoStars.toString();
: unleashRepoStars?.toString() || '';
return formattedStars;
};
@ -209,54 +209,181 @@ class="header-github-link"
},
],
footer: {
style: 'dark',
links: [
{
title: 'Product',
title: 'Server SDKs',
items: [
{
label: 'Docs',
to: '/',
label: 'Node.js',
to: '/reference/sdks/node',
},
{
label: 'Unleash on GitHub',
href: 'https://github.com/Unleash/unleash',
label: 'Java',
to: '/reference/sdks/java',
},
{
label: 'Roadmap',
href: 'https://github.com/orgs/Unleash/projects/10',
label: 'Go',
to: '/reference/sdks/go',
},
{
label: 'Unleash help center',
href: 'https://getunleash.zendesk.com/hc/en-gb',
label: 'Rust',
to: '/reference/sdks/rust',
},
{
label: 'Ruby',
to: '/reference/sdks/ruby',
},
{
label: 'Python',
to: '/reference/sdks/python',
},
{
label: '.NET',
to: '/reference/sdks/dotnet',
},
{
label: 'PHP',
to: '/reference/sdks/php',
},
{
label: 'All SDKs',
to: '/reference/sdks',
},
],
},
{
title: 'Community',
title: 'Frontend SDKs',
items: [
{
label: 'GitHub discussions',
href: 'https://github.com/unleash/unleash/discussions/',
label: 'JavaScript',
to: '/reference/sdks/javascript-browser',
},
{
label: 'Slack',
href: 'https://slack.unleash.run/',
label: 'React',
to: '/reference/sdks/react',
},
{
label: 'Stack Overflow',
href: 'https://stackoverflow.com/questions/tagged/unleash',
label: 'Next.js',
to: '/reference/sdks/next-js',
},
{
label: 'Twitter',
href: 'https://twitter.com/getunleash',
label: 'Vue',
to: '/reference/sdks/vue',
},
{
label: 'iOS',
to: '/reference/sdks/ios-proxy',
},
{
label: 'Android',
to: '/reference/sdks/android-proxy',
},
{
label: 'Flutter',
to: '/reference/sdks/flutter',
},
],
},
{
title: 'Feature Flag use cases',
items: [
{
label: 'Secure, scalable feature flags',
to: '/topics/feature-flags/feature-flag-best-practices',
},
{
label: 'Rollbacks',
href: 'https://www.getunleash.io/feature-flag-use-cases-rollbacks',
},
{
label: 'FedRAMP, SOC2, ISO2700 compliance',
to: '/using-unleash/compliance/compliance-overview',
},
{
label: 'Progressive or gradual rollouts',
to: '/feature-flag-tutorials/use-cases/gradual-rollout',
},
{
label: 'Trunk-based development',
to: '/feature-flag-tutorials/use-cases/trunk-based-development',
},
{
label: 'Software kill switches',
href: 'https://www.getunleash.io/feature-flag-use-cases-software-kill-switches',
},
{
label: 'A/B testing',
to: '/feature-flag-tutorials/use-cases/a-b-testing',
},
{
label: 'Feature management',
href: 'https://www.getunleash.io/blog/feature-management',
},
{
label: 'Canary releases',
href: 'https://www.getunleash.io/blog/canary-deployment-what-is-it',
},
],
},
{
title: 'Product',
items: [
{
label: 'Quickstart',
to: '/quickstart',
},
{
label: 'Unleash architecture',
to: '/understanding-unleash/unleash-overview',
},
{
label: 'Pricing',
href: 'https://www.getunleash.io/pricing',
},
{
label: 'Open live demo',
href: 'https://app.unleash-hosted.com/demo/login',
},
{
label: 'Open source',
href: 'https://www.getunleash.io/open-source',
},
{
label: 'Enterprise feature management platform',
href: 'https://www.getunleash.io/enterprise-feature-management-platform',
},
{
label: 'Unleash vs LaunchDarkly',
href: 'https://www.getunleash.io/unleash-vs-launchdarkly',
},
],
},
{
title: 'Support',
items: [
{
label: 'Help center',
href: 'https://www.getunleash.io/support',
},
{
label: 'Status',
href: 'https://unleash.instatus.com',
},
{
label: 'Roadmap',
href: 'https://github.com/orgs/Unleash/projects/10/views/1',
},
{
label: 'Changelog',
href: 'https://github.com/Unleash/unleash/releases',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Unleash. Built with Docusaurus.`,
logo: {
src: 'img/logo.svg',
src: 'img/unleash_logo_dark_no_label.svg',
srcDark: 'img/unleash_logo_white_no_label.svg',
alt: 'Unleash logo',
},
},

View File

@ -64,6 +64,8 @@ html[data-theme="light"] {
--docsearch-searchbox-background: var(--ifm-background-color);
--unleash-color-header-separator: var(--unleash-color-green);
--unleash-color-footer-icon: #657a80;
}
html[data-theme="dark"] {
@ -108,6 +110,8 @@ html[data-theme="dark"] {
--unleash-logo: url("/img/unleash_logo_white.svg");
--unleash-color-header-separator: rgba(255, 255, 255, 0.3);
--unleash-color-footer-icon: var(--ifm-font-color-base);
}
/* navbar */
@ -353,7 +357,161 @@ main .theme-doc-breadcrumbs {
}
.footer {
display: flex;
z-index: 1;
font-size: 12px;
}
.footer-body {
display: flex;
flex-direction: column;
width: 100%;
padding: 0 15px;
}
.footer-content {
display: flex;
justify-content: space-between;
gap: 30px;
}
.footer-content > .container {
display: flex;
justify-content: end;
padding: 0;
margin: 0;
}
.footer__links {
gap: 16px;
width: 100%;
max-width: 1000px;
}
.footer-separator {
height: 1px;
background: #d9d9d9;
margin-top: 30px;
margin-bottom: 30px;
}
html[data-theme="dark"] .footer-separator {
background: #333;
}
.footer-description {
display: flex;
align-items: start;
gap: 16px;
max-width: 500px;
height: 100%;
}
.footer-description .footer__logo {
width: 50px;
margin-top: 0px;
}
.footer-description p {
line-height: 18px;
font-weight: 400;
margin-bottom: 0;
}
.footer-description > div {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 16px;
}
.footer-description .link-icons {
display: flex;
gap: 16px;
}
.footer-description .link-icons img {
max-width: unset;
transition: opacity 0.2s;
}
.footer-description .link-icons img:hover {
opacity: 0.8;
}
.footer__col {
padding: 0;
}
.footer__title {
font-size: 12px;
margin-bottom: 4px;
}
.footer__item {
margin-top: 10px;
}
.footer__link-item {
line-height: 18px;
}
.footer__link-item > svg {
display: none;
}
.footer-bottom {
display: flex;
justify-content: space-between;
}
.footer-bottom-made-with-love {
display: flex;
align-items: center;
gap: 4px;
}
.footer-show-large {
display: block;
}
.footer-show-small {
display: none;
}
@media (max-width: 1300px) {
.footer-show-large {
display: none;
}
.footer-show-small {
display: block;
}
.footer-content {
flex-direction: column;
align-items: center;
}
.footer-content > .container {
display: flex;
justify-content: space-around;
padding: 0;
margin: 0;
}
.footer-description > div {
flex: unset;
align-items: center;
}
}
@media (max-width: 500px) {
.footer-description {
flex-direction: column;
align-items: center;
}
}
.navbar {

View File

@ -0,0 +1,13 @@
import type { Props } from '@theme/Footer/Copyright';
export default function FooterCopyright({ copyright }: Props): JSX.Element {
return (
<div
className='footer__copyright'
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
// biome-ignore lint/security/noDangerouslySetInnerHtml: from swizzled docusaurus component
dangerouslySetInnerHTML={{ __html: copyright }}
/>
);
}

View File

@ -0,0 +1,124 @@
import clsx from 'clsx';
import type { Props } from '@theme/Footer/Layout';
import ThemedImage from '@theme/ThemedImage';
import HeartIcon from '../icons/heart.svg';
import { useBaseUrlUtils } from '@docusaurus/useBaseUrl';
type IconLink = {
href: string;
src: string;
srcDark: string;
alt: string;
};
const linkIcons: IconLink[] = [
{
href: 'https://github.com/unleash/unleash',
src: '/img/footer/githubLight.png',
srcDark: '/img/footer/githubDark.png',
alt: 'GitHub',
},
{
href: 'https://www.linkedin.com/company/getunleash',
src: '/img/footer/linkedinLight.png',
srcDark: '/img/footer/linkedinDark.png',
alt: 'LinkedIn',
},
{
href: 'https://twitter.com/getunleash',
src: '/img/footer/twitterLight.png',
srcDark: '/img/footer/twitterDark.png',
alt: 'Twitter',
},
{
href: 'https://slack.unleash.run',
src: '/img/footer/slackLight.png',
srcDark: '/img/footer/slackDark.png',
alt: 'Slack',
},
{
href: 'https://stackoverflow.com/questions/tagged/unleash',
src: '/img/footer/stackoverflowLight.png',
srcDark: '/img/footer/stackoverflowDark.png',
alt: 'Stack Overflow',
},
{
href: 'https://www.youtube.com/channel/UCJjGVOc5QBbEje-r7nZEa4A',
src: '/img/footer/youtubeLight.png',
srcDark: '/img/footer/youtubeDark.png',
alt: 'YouTube',
},
];
export default function FooterLayout({
style,
links,
logo,
copyright,
}: Props): JSX.Element {
const { withBaseUrl } = useBaseUrlUtils();
const description = (
<div className='footer-description'>
{logo}
<div>
<p>
Unleash reduces the risk of releasing new features, drives
innovation by streamlining the software release process, and
increases revenue by optimizing end-user experience. While
we serve the needs of the world's largest, most
security-conscious organizations, we are also rated the
Easiest Feature Management system to use by G2.
</p>
<div className='link-icons'>
{linkIcons.map(({ href, src, srcDark, alt }) => (
<a
key={alt}
href={href}
target='_blank'
rel='noopener noreferrer'
>
<ThemedImage
alt={alt}
sources={{
light: withBaseUrl(src),
dark: withBaseUrl(srcDark),
}}
width={40}
height={40}
/>
</a>
))}
</div>
</div>
</div>
);
return (
<footer
className={clsx('footer', {
'footer--dark': style === 'dark',
})}
>
<div className='footer-body'>
<div className='footer-content'>
<div className='footer-show-large'>{description}</div>
<div className='container container-fluid'>{links}</div>
<div className='footer-show-small'>{description}</div>
</div>
<div className='footer-separator' />
<div className='footer-bottom'>
<div className='footer-bottom-made-with-love'>
<HeartIcon /> Made in a cosy atmosphere in the Nordic
countries.
</div>
{copyright && (
<div className='footer__bottom'>{copyright}</div>
)}
</div>
</div>
</footer>
);
}

View File

@ -0,0 +1,28 @@
import Link from '@docusaurus/Link';
import useBaseUrl from '@docusaurus/useBaseUrl';
import isInternalUrl from '@docusaurus/isInternalUrl';
import IconExternalLink from '@theme/Icon/ExternalLink';
import type { Props } from '@theme/Footer/LinkItem';
export default function FooterLinkItem({ item }: Props): JSX.Element {
const { to, href, label, prependBaseUrlToHref, ...props } = item;
const toUrl = useBaseUrl(to);
const normalizedHref = useBaseUrl(href, { forcePrependBaseUrl: true });
return (
<Link
className='footer__link-item'
{...(href
? {
href: prependBaseUrlToHref ? normalizedHref : href,
}
: {
to: toUrl,
})}
{...props}
>
{label}
{href && !isInternalUrl(href) && <IconExternalLink />}
</Link>
);
}

View File

@ -0,0 +1,46 @@
import LinkItem from '@theme/Footer/LinkItem';
import type { Props } from '@theme/Footer/Links/MultiColumn';
type ColumnType = Props['columns'][number];
type ColumnItemType = ColumnType['items'][number];
function ColumnLinkItem({ item }: { item: ColumnItemType }) {
return item.html ? (
<li
className='footer__item'
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
// biome-ignore lint/security/noDangerouslySetInnerHtml: from swizzled docusaurus component
dangerouslySetInnerHTML={{ __html: item.html }}
/>
) : (
<li key={item.href ?? item.to} className='footer__item'>
<LinkItem item={item} />
</li>
);
}
function Column({ column }: { column: ColumnType }) {
return (
<div className='col footer__col'>
<div className='footer__title'>{column.title}</div>
<ul className='footer__items clean-list'>
{column.items.map((item, i) => (
<ColumnLinkItem key={i} item={item} />
))}
</ul>
</div>
);
}
export default function FooterLinksMultiColumn({
columns,
}: Props): JSX.Element {
return (
<div className='row footer__links'>
{columns.map((column, i) => (
<Column key={i} column={column} />
))}
</div>
);
}

View File

@ -0,0 +1,36 @@
import React from 'react';
import LinkItem from '@theme/Footer/LinkItem';
import type { Props } from '@theme/Footer/Links/Simple';
function Separator() {
return <span className='footer__link-separator'>·</span>;
}
function SimpleLinkItem({ item }: { item: Props['links'][number] }) {
return item.html ? (
<span
className='footer__link-item'
// Developer provided the HTML, so assume it's safe.
// eslint-disable-next-line react/no-danger
// biome-ignore lint/security/noDangerouslySetInnerHtml: from swizzled docusaurus component
dangerouslySetInnerHTML={{ __html: item.html }}
/>
) : (
<LinkItem item={item} />
);
}
export default function FooterLinksSimple({ links }: Props): JSX.Element {
return (
<div className='footer__links text--center'>
<div className='footer__links'>
{links.map((item, i) => (
<React.Fragment key={i}>
<SimpleLinkItem item={item} />
{links.length !== i + 1 && <Separator />}
</React.Fragment>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
import { isMultiColumnFooterLinks } from '@docusaurus/theme-common';
import FooterLinksMultiColumn from '@theme/Footer/Links/MultiColumn';
import FooterLinksSimple from '@theme/Footer/Links/Simple';
import type { Props } from '@theme/Footer/Links';
export default function FooterLinks({ links }: Props): JSX.Element {
return isMultiColumnFooterLinks(links) ? (
<FooterLinksMultiColumn columns={links} />
) : (
<FooterLinksSimple links={links} />
);
}

View File

@ -0,0 +1,39 @@
import clsx from 'clsx';
import Link from '@docusaurus/Link';
import { useBaseUrlUtils } from '@docusaurus/useBaseUrl';
import ThemedImage from '@theme/ThemedImage';
import type { Props } from '@theme/Footer/Logo';
import styles from './styles.module.css';
function LogoImage({ logo }: Props) {
const { withBaseUrl } = useBaseUrlUtils();
const sources = {
light: withBaseUrl(logo.src),
dark: withBaseUrl(logo.srcDark ?? logo.src),
};
return (
<ThemedImage
className={clsx('footer__logo', logo.className)}
alt={logo.alt}
sources={sources}
width={logo.width}
height={logo.height}
style={logo.style}
/>
);
}
export default function FooterLogo({ logo }: Props): JSX.Element {
return logo.href ? (
<Link
href={logo.href}
className={styles.footerLogoLink}
target={logo.target}
>
<LogoImage logo={logo} />
</Link>
) : (
<LogoImage logo={logo} />
);
}

View File

@ -0,0 +1,9 @@
.footerLogoLink {
opacity: 0.5;
transition: opacity var(--ifm-transition-fast)
var(--ifm-transition-timing-default);
}
.footerLogoLink:hover {
opacity: 1;
}

View File

@ -0,0 +1,64 @@
<svg
width='21'
height='21'
viewBox='0 0 21 21'
xmlns='http://www.w3.org/2000/svg'
>
<title>Heart</title>
<path
d='M17.0674 4.53516V10.5352H19.0674V4.53516H17.0674Z'
fill='#817AFE'
/>
<path
d='M1.06738 4.53516L1.06738 10.5352H3.06738L3.06738 4.53516H1.06738Z'
fill='#817AFE'
/>
<path
d='M17.0674 2.53516H13.0674V10.5352H17.0674V2.53516Z'
fill='#817AFE'
/>
<path
d='M7.06738 2.53516H3.06738V10.5352H7.06738V2.53516Z'
fill='#817AFE'
/>
<path
d='M13.0674 12.5352H5.06738V14.5352H13.0674V12.5352Z'
fill='#817AFE'
/>
<path
d='M15.0674 10.5352H3.06738V12.5352H15.0674V10.5352Z'
fill='#817AFE'
/>
<path
d='M11.0674 6.53516H9.06738V10.5352H11.0674V6.53516Z'
fill='#817AFE'
/>
<path
d='M13.0674 4.53516H11.0674V10.5352H13.0674V4.53516Z'
fill='#817AFE'
/>
<path
d='M9.06738 4.53516H7.06738V10.5352H9.06738V4.53516Z'
fill='#817AFE'
/>
<path
d='M11.0674 14.5352H7.06738V16.5352H11.0674V14.5352Z'
fill='#817AFE'
/>
<path
d='M11.0674 16.5352H9.06738V18.5352H11.0674V16.5352Z'
fill='#817AFE'
/>
<path
d='M13.0674 14.5352H11.0674V16.5352H13.0674V14.5352Z'
fill='#817AFE'
/>
<path
d='M15.0674 12.5352H13.0674V14.5352H15.0674V12.5352Z'
fill='#817AFE'
/>
<path
d='M17.0674 10.5352H15.0674V12.5352H17.0674V10.5352Z'
fill='#817AFE'
/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,26 @@
import React from 'react';
import { useThemeConfig } from '@docusaurus/theme-common';
import FooterLinks from '@theme/Footer/Links';
import FooterLogo from '@theme/Footer/Logo';
import FooterCopyright from '@theme/Footer/Copyright';
import FooterLayout from '@theme/Footer/Layout';
function Footer(): JSX.Element | null {
const { footer } = useThemeConfig();
if (!footer) {
return null;
}
const { copyright, links, logo, style } = footer;
return (
<FooterLayout
style={style}
links={links && links.length > 0 && <FooterLinks links={links} />}
logo={logo && <FooterLogo logo={logo} />}
copyright={copyright && <FooterCopyright copyright={copyright} />}
/>
);
}
export default React.memo(Footer);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,6 @@
<svg width="160" height="160" viewBox="0 0 160 160" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M80 160C124.183 160 160 124.183 160 80C160 35.8172 124.183 0 80 0C35.8172 0 0 35.8172 0 80C0 124.183 35.8172 160 80 160Z" fill="#1A4049"/>
<path d="M91.43 45.71V91.43H114.28V45.71H91.43ZM68.57 68.57V45.71H45.71V114.28H91.43V91.43H68.57V68.57Z" fill="white"/>
<path d="M91.43 45.71V91.43H114.28V45.71H91.43ZM68.57 68.57V45.71H45.71V114.28H91.43V91.43H68.57V68.57Z" fill="white"/>
<path d="M91.4299 91.43H114.29V114.29H91.4299V91.43Z" fill="#817AFE"/>
</svg>

After

Width:  |  Height:  |  Size: 566 B

View File

@ -0,0 +1,6 @@
<svg width="161" height="161" viewBox="0 0 161 161" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M80.6201 160.62C124.803 160.62 160.62 124.803 160.62 80.62C160.62 36.4372 124.803 0.619995 80.6201 0.619995C36.4373 0.619995 0.620117 36.4372 0.620117 80.62C0.620117 124.803 36.4373 160.62 80.6201 160.62Z" fill="white"/>
<path d="M92.0501 46.33V92.05H114.9V46.33H92.0501ZM69.1901 69.19V46.33H46.3301V114.9H92.0501V92.05H69.1901V69.19Z" fill="white"/>
<path d="M92.0501 46.33V92.05H114.9V46.33H92.0501ZM69.1901 69.19V46.33H46.3301V114.9H92.0501V92.05H69.1901V69.19Z" fill="#1A4049"/>
<path d="M92.05 92.05H114.91V114.91H92.05V92.05Z" fill="#817AFE"/>
</svg>

After

Width:  |  Height:  |  Size: 666 B