/******************************************************************************** * 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, lazy, Suspense, useContext, useEffect, useState } from 'react'; import { Routes, Route, useNavigate } from 'react-router'; import { Box } from '@mui/material'; import { styled } from '@mui/material/styles'; import { Banner } from '../components/banner'; import { ShortcutsModal } from '../components/shortcuts-modal'; import { MainContext } from '../context'; import { useSearch } from '../hooks/use-search'; import { ExtensionTintProvider } from '../context/extension-tint-context'; import { useShortcut } from '../hooks/use-shortcut'; import { getCookieValueByKey, setCookie } from '../utils'; import { ExtensionListRoutes } from '../pages/extension-list/extension-list-routes'; import { UserSettingsRoutes } from '../pages/user/user-settings-routes'; import { NamespaceDetailRoutes } from '../pages/namespace-detail/namespace-detail-routes'; import { ExtensionDetailRoutes } from '../pages/extension-detail/extension-detail-routes'; import { ExtensionDetail } from '../pages/extension-detail/extension-detail'; import { HomePage } from '../pages/home/home-page'; import { SearchPage } from '../pages/search/search-page'; import { NamespaceDetail } from '../pages/namespace-detail/namespace-detail'; import { NotFound } from '../not-found'; import { NAVBAR_HEIGHT } from '../default/theme'; import { AppNavbar } from './app-navbar'; import { AppFooter } from './app-footer'; import { ScrollToTop } from './scroll-to-top'; const UserSettings = lazy(() => import('../pages/user/user-settings').then(m => ({ default: m.UserSettings }))); const Wrapper = styled(Box)({ display: 'flex', flexDirection: 'column', position: 'relative', minHeight: '100vh' }); const AppLayoutContent: FunctionComponent = props => { const { pageSettings } = useContext(MainContext); const { additionalRoutes: AdditionalRoutes, banner: BannerComponent, footer } = pageSettings.elements; // The legacy footer is fixed to the viewport bottom; pad the content to stay clear of it. const legacyFooterHeight = typeof footer === 'object' && 'content' in footer ? footer.props?.footerHeight : 0; const navigate = useNavigate(); const { search } = useSearch(); const [isBannerOpen, setIsBannerOpen] = useState(false); const [shortcutsOpen, setShortcutsOpen] = useState(false); useEffect(() => { const banner = pageSettings.elements.banner; if (!banner) return; if (banner.cookie && getCookieValueByKey(banner.cookie.key) === banner.cookie.value) return; // Start collapsed on load, then slide open a beat later for a gentle entrance. const timer = setTimeout(() => setIsBannerOpen(true), 600); return () => clearTimeout(timer); }, []); // Global navigation shortcuts with no on-screen affordance. Shortcuts tied to a // visible control (?, d, p) are registered where that control renders, so a // customized footer/menu brings its own instead of inheriting ours. useShortcut({ key: 'h', label: 'Go to home', order: 4, callback: () => navigate('/') }); useShortcut({ key: 's', label: 'Go to search', order: 5, // Through `search` (not a bare navigate) so the field clears with the URL. callback: () => search({ query: '' }) }); const onDismissBannerButtonClick = () => { const onClose = pageSettings.elements.banner?.props?.onClose; if (onClose) onClose(); const cookie = pageSettings.elements.banner?.cookie; if (cookie) setCookie(cookie); setIsBannerOpen(false); }; return ( {BannerComponent ? ( ) : null} {/* Mobile: fill the viewport so the (screen-tall) footer stays below the fold. Desktop: flexGrow alone sticks the footer to the viewport bottom. A flex column so pages can opt into stretching over the leftover space (e.g. the search page runs its sidebar seam down to the footer). */} . pb: legacyFooterHeight ? `${legacyFooterHeight + 24}px` : 0 }}> } /> } /> } /> } /> } /> } /> } /> {AdditionalRoutes ?? null} } /> setShortcutsOpen(true)} /> setShortcutsOpen(false)} /> ); }; // Keyboard shortcuts and search now live app-wide in AppProviders; this keeps // only the feature-scoped tint provider. export const AppLayout: FunctionComponent = props => ( ); export interface AppLayoutProps { userLoading: boolean; }