/********************************************************************************
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* https://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
********************************************************************************/
import { FunctionComponent, useContext, useState } from 'react';
import { Box, Container, Typography } from '@mui/material';
import { styled } from '@mui/material/styles';
import type { Theme } from '@mui/material/styles';
import { Link as RouteLink } from 'react-router';
import { KbdKey } from '../components/kbd-key';
import { useShortcut } from '../hooks/use-shortcut';
import { Eyebrow, accentHover, focusOutline } from '../components/page-primitives';
import { MONO_FONT } from '../default/theme';
import { CustomFooterSettings, StructuredFooterSettings } from '../page-settings';
import { MainContext } from '../context';
const footerLinkStyles = ({ theme }: { theme: Theme }) => ({
fontSize: '0.8125rem',
color: theme.palette.text.secondary,
textDecoration: 'none',
display: 'block',
'&:hover': { color: theme.palette.secondary.light },
...focusOutline(theme)
});
const FooterLink = styled('a')(footerLinkStyles);
const FooterRouteLink = styled(RouteLink)(footerLinkStyles);
const SocialIconButton = styled('a')(({ theme }) => ({
width: 34,
height: 34,
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
color: theme.palette.text.secondary,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
textDecoration: 'none',
transition: 'border-color 0.14s, color 0.14s',
...accentHover(theme),
...focusOutline(theme)
}));
const ShortcutsButton = styled('button')(({ theme }) => ({
background: 'none',
border: 'none',
cursor: 'pointer',
padding: 0,
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
fontSize: '0.75rem',
color: theme.palette.text.disabled,
'&:hover': { color: theme.palette.secondary.light },
...focusOutline(theme)
}));
/** Legacy footer chrome: fixed to the viewport bottom as on pre-redesign layouts, hover toggles `expanded`. */
const LegacyFooterRoot = styled('footer')(({ theme }) => ({
position: 'fixed',
bottom: 0,
zIndex: 50,
width: '100%',
padding: `${theme.spacing(1)} ${theme.spacing(2)}`,
backgroundColor: theme.palette.background.paper,
boxShadow: '0px -2px 6px 0px rgba(0, 0, 0, 0.5)',
backgroundImage:
theme.palette.mode === 'dark'
? 'linear-gradient(rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.08))'
: undefined
}));
const LegacyFooter: FunctionComponent<{ footer: CustomFooterSettings }> = ({ footer }) => {
const Content = footer.content;
const [expanded, setExpanded] = useState(false);
return (
setExpanded(true)} onMouseLeave={() => setExpanded(false)}>
);
};
export interface AppFooterProps {
onOpenShortcuts: () => void;
}
export const AppFooter: FunctionComponent = ({ onOpenShortcuts }) => {
const { pageSettings, version } = useContext(MainContext);
const footer = pageSettings.elements.footer;
// The shortcuts button (and its ? hint) only renders in the structured footer,
// so register the shortcut here and disable it for custom/legacy footers.
const isStructuredFooter = typeof footer !== 'function' && !(footer && 'content' in footer);
useShortcut({
key: '?',
label: 'Show keyboard shortcuts',
order: 0,
callback: onOpenShortcuts,
enabled: isStructuredFooter
});
if (typeof footer === 'function') {
const CustomFooter = footer;
return ;
}
if (footer && 'content' in footer) {
return ;
}
return ;
};
interface StructuredFooterProps {
footer?: StructuredFooterSettings;
version: string | null;
onOpenShortcuts: () => void;
}
const StructuredFooter: FunctionComponent = ({ footer, version, onOpenShortcuts }) => (
{footer && (footer.brand || footer.columns) && (
{footer.brand && (
{footer.brand.logo}
{footer.brand.name}
{footer.brand.description && (
{footer.brand.description}
)}
{footer.social && footer.social.length > 0 && (
{footer.social.map(s => (
{s.icon}
))}
)}
)}
{footer.columns?.map((column, ci) => (
{column.heading}
{column.links.map((l, li) =>
!l.external && l.href.startsWith('/') && !l.href.startsWith('//') ? (
{l.label}
) : (
{l.label}
)
)}
))}
)}
{footer?.copyright}
{footer?.extra}
Keyboard shortcuts
?
{footer?.showVersion && version && (
{version}
)}
);