Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions webui/src/components/page-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/********************************************************************************
* 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 { forwardRef } from 'react';
import { Container, ContainerProps } from '@mui/material';

export interface PageContainerProps extends ContainerProps {
/** Full-bleed: drop the width cap and gutters so nested containers own their centering (e.g. the home). */
fluid?: boolean;
/** No top margin — content hugs the nav, or an inner element supplies its own. */
flushTop?: boolean;
/** No bottom margin — content meets the footer line, or an inner element supplies its own. */
flushBottom?: boolean;
}

/**
* A MUI `Container` plus the standard page margins (a top offset below the nav and
* a gap above the footer). For page-level content; non-pages or custom spacing
* should use `Container` directly.
*/
export const PageContainer = forwardRef<HTMLDivElement, PageContainerProps>(function PageContainer(
{ fluid, flushTop, flushBottom, sx, ...props },
ref
) {
return (
<Container
ref={ref}
maxWidth={fluid ? false : 'xl'}
disableGutters={fluid}
sx={[
{
pt: flushTop ? 0 : { xs: '2.75rem', sm: '4.875rem' },
pb: flushBottom ? 0 : { xs: '2.5rem', sm: '4rem' }
},
...(Array.isArray(sx) ? sx : [sx])
]}
{...props}
/>
);
});
20 changes: 7 additions & 13 deletions webui/src/components/page-primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,13 @@
import { Box, Typography } from '@mui/material';
import { alpha, styled, Theme } from '@mui/material/styles';

/** Max width of the centered content column shared by every page section. */
export const CONTENT_MAX_WIDTH = 1320;

/**
* Horizontally-centered content column with responsive gutters. The single
* source of truth for page width so sections stay aligned across the app.
*/
export const Section = styled(Box)(({ theme }) => ({
maxWidth: CONTENT_MAX_WIDTH,
marginInline: 'auto',
paddingInline: '1.75rem',
[theme.breakpoints.down('sm')]: {
paddingInline: '1rem'
/** Normalized gap between stacked sections; the owl selector skips the first (and any null) child. */
export const SectionStack = styled(Box)(({ theme }) => ({
'& > * + *': {
marginTop: '2.5rem',
[theme.breakpoints.down('sm')]: {
marginTop: '1.5rem'
}
}
}));

Expand Down
2 changes: 1 addition & 1 deletion webui/src/default/theme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export default function createDefaultTheme(themeType: 'light' | 'dark'): Theme {
}
},
breakpoints: {
values: { xs: 0, sm: 550, md: 800, lg: 1040, xl: 1240 }
values: { xs: 0, sm: 550, md: 800, lg: 1040, xl: 1320 }
},
components: {
MuiAccordion: {
Expand Down
1 change: 1 addition & 0 deletions webui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export {
} from './pages/home/use-home-data';
export { ExtensionCard, type ExtensionCardProps } from './components/extension-card';
export * from './components/page-primitives';
export * from './components/page-container';
// Leaf hook modules keep their helpers private, so `export *` exposes only the
// public hook plus its types (e.g. useSearch + SearchFilter).
export * from './hooks/use-search';
Expand Down
14 changes: 8 additions & 6 deletions webui/src/layout/app-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
********************************************************************************/

import { FunctionComponent, useContext, useState } from 'react';
import { Box, Typography } from '@mui/material';
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-dom';
import { KbdKey } from '../components/kbd-key';
import { useShortcut } from '../hooks/use-shortcut';
import { Section, Eyebrow, accentHover, focusOutline } from '../components/page-primitives';
import { Eyebrow, accentHover, focusOutline } from '../components/page-primitives';
import { MONO_FONT } from '../default/theme';
import { CustomFooterSettings, StructuredFooterSettings } from '../page-settings';
import { MainContext } from '../context';
Expand Down Expand Up @@ -131,7 +131,8 @@ const StructuredFooter: FunctionComponent<StructuredFooterProps> = ({ footer, ve
component='footer'
sx={{ mt: 'auto', borderTop: '1px solid', borderColor: 'divider', bgcolor: 'background.paper' }}>
{footer && (footer.brand || footer.columns) && (
<Section
<Container
maxWidth='xl'
sx={{
pt: '3rem',
pb: '1.875rem',
Expand Down Expand Up @@ -191,9 +192,10 @@ const StructuredFooter: FunctionComponent<StructuredFooterProps> = ({ footer, ve
</Box>
</Box>
))}
</Section>
</Container>
)}
<Section
<Container
maxWidth='xl'
sx={{
py: '1.25rem',
borderTop: footer?.brand || footer?.columns ? '1px solid' : 'none',
Expand All @@ -217,6 +219,6 @@ const StructuredFooter: FunctionComponent<StructuredFooterProps> = ({ footer, ve
</Typography>
)}
</Box>
</Section>
</Container>
</Box>
);
4 changes: 2 additions & 2 deletions webui/src/layout/app-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ const AppLayoutContent: FunctionComponent<AppLayoutProps> = props => {
sx={{
flexGrow: 1,
minHeight: { xs: `calc(100vh - ${NAVBAR_HEIGHT})`, sm: 0 },
// Legacy footer is fixed, so pad by its height; otherwise leave a gap above the footer divider.
pb: legacyFooterHeight ? `${legacyFooterHeight + 24}px` : { xs: '2.5rem', sm: '4rem' }
// Only the fixed legacy footer needs clearance; the in-flow footer's gap lives on <PageContainer>.
pb: legacyFooterHeight ? `${legacyFooterHeight + 24}px` : 0
}}>
<Suspense fallback={null}>
<Routes>
Expand Down
7 changes: 4 additions & 3 deletions webui/src/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { FunctionComponent } from 'react';
import { Box, Container, Typography } from '@mui/material';
import { Box, Typography } from '@mui/material';
import BrokenImageIcon from '@mui/icons-material/BrokenImage';
import { PageContainer } from './components/page-container';

export const NotFound: FunctionComponent = () => {
return (
<Container>
<PageContainer flushTop>
<Box height='30vh' display='flex' flexWrap='wrap' justifyContent='center' alignItems='center'>
<Typography variant='h3'>Oooups...this is a 404 page.</Typography>
<BrokenImageIcon sx={{ fontSize: '4rem', flexBasis: '100%' }} />
</Box>
</Container>
</PageContainer>
);
};
10 changes: 4 additions & 6 deletions webui/src/pages/extension-detail/extension-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { useShortcut } from '../../hooks/use-shortcut';
import { NAVBAR_HEIGHT, NAVBAR_HEIGHT_PX } from '../../default/theme';
import { useSetExtensionTint } from '../../context/extension-tint-context';
import { accentHover, focusOutline } from '../../components/page-primitives';
import { PageContainer } from '../../components/page-container';

// Category-pill look for the sticky tabs, floating over the nav bar's blur fan;
// the translucent fill matches the nav search field's treatment.
Expand Down Expand Up @@ -505,7 +506,7 @@ export const ExtensionDetail: FunctionComponent = () => {
{extension && (
<>
<ExtensionHeader extension={extension} bandRef={bandRef} />
<Container maxWidth='xl'>
<PageContainer flushTop>
<Tabs
value={activeTab}
variant='scrollable'
Expand Down Expand Up @@ -567,10 +568,7 @@ export const ExtensionDetail: FunctionComponent = () => {
state={{ preserveScroll: true }}
/>
</Tabs>
{/* Owns the space below the pinned pills for every tab, so the panels
don't each set their own; min-height keeps the preserved scroll
position valid while a panel loads. */}
<Box sx={{ minHeight: '100vh', pt: 2 }}>
<Box sx={{ pt: 2 }}>
<Routes>
<Route
path={ExtensionTab.REVIEWS}
Expand All @@ -591,7 +589,7 @@ export const ExtensionDetail: FunctionComponent = () => {
/>
</Routes>
</Box>
</Container>
</PageContainer>
</>
)}
{error && (
Expand Down
8 changes: 4 additions & 4 deletions webui/src/pages/home/browse-categories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
********************************************************************************/

import { FunctionComponent } from 'react';
import { Box } from '@mui/material';
import { Box, Container } from '@mui/material';
import { ExtensionCategory } from '../../extension-registry-types';
import { CATEGORY_ICONS, DefaultCategoryIcon } from '../../components/categories';
import { CategoryPill } from '../../components/category-pill';
import { CategoryCard } from '../../components/category-card';
import { Section, Eyebrow } from '../../components/page-primitives';
import { Eyebrow } from '../../components/page-primitives';
import { useSearch } from '../../hooks/use-search';
import { useHomeCategories } from './use-home-data';

Expand All @@ -38,7 +38,7 @@ export const BrowseCategories: FunctionComponent<BrowseCategoriesProps> = props
return null;
}
return (
<Section component='section' sx={{ mt: { xs: '1.375rem', sm: '2.25rem' } }}>
<Container maxWidth='xl' component='section'>
<Eyebrow sx={{ mb: { xs: '0.75rem', sm: '1.125rem' } }}>Browse by category</Eyebrow>
<Box
sx={{
Expand Down Expand Up @@ -77,6 +77,6 @@ export const BrowseCategories: FunctionComponent<BrowseCategoriesProps> = props
/>
))}
</Box>
</Section>
</Container>
);
};
7 changes: 3 additions & 4 deletions webui/src/pages/home/curated-sections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
********************************************************************************/

import { FunctionComponent } from 'react';
import { Box, Typography } from '@mui/material';
import { Box, Container, Typography } from '@mui/material';
import { ExtensionCard } from '../../components/extension-card';
import { Section } from '../../components/page-primitives';
import { HomeCuratedSection } from '../../page-settings';
import { useSearch } from '../../hooks/use-search';
import { CURATED_SIZE, DEFAULT_CURATED_SECTIONS, useCuratedRows } from './use-home-data';
Expand All @@ -38,7 +37,7 @@ export const CuratedSections: FunctionComponent<CuratedSectionsProps> = props =>
return null;
}
return (
<Section component='section' key={row.title} sx={{ mt: { xs: '2.25rem', sm: '3.375rem' } }}>
<Container maxWidth='xl' component='section' key={row.title}>
<Box
sx={{
display: 'flex',
Expand Down Expand Up @@ -97,7 +96,7 @@ export const CuratedSections: FunctionComponent<CuratedSectionsProps> = props =>
/>
))}
</Box>
</Section>
</Container>
);
})}
</>
Expand Down
8 changes: 4 additions & 4 deletions webui/src/pages/home/get-involved.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
********************************************************************************/

import { FunctionComponent, ReactNode } from 'react';
import { Box, Typography } from '@mui/material';
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-dom';
import { HomeInvolvementCard } from '../../page-settings';
import { Section, Eyebrow, focusOutline } from '../../components/page-primitives';
import { Eyebrow, focusOutline } from '../../components/page-primitives';

const GetInvolvedCard = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
Expand Down Expand Up @@ -74,7 +74,7 @@ export const GetInvolved: FunctionComponent<GetInvolvedProps> = ({ heading, card
return null;
}
return (
<Section component='section' sx={{ mt: { xs: '3rem', sm: '4.5rem' } }}>
<Container maxWidth='xl' component='section'>
<Eyebrow sx={{ letterSpacing: '0.1em', mb: { xs: '0.875rem', sm: '1.25rem' } }}>
{heading ?? 'Get Involved'}
</Eyebrow>
Expand Down Expand Up @@ -120,6 +120,6 @@ export const GetInvolved: FunctionComponent<GetInvolvedProps> = ({ heading, card
);
})}
</Box>
</Section>
</Container>
);
};
14 changes: 4 additions & 10 deletions webui/src/pages/home/hero-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import {
useRef,
useState
} from 'react';
import { Box, ButtonBase, Typography } from '@mui/material';
import { Box, ButtonBase, Container, Typography } from '@mui/material';
import { useLocation } from 'react-router-dom';
import { flushSync } from 'react-dom';
import { styled, alpha } from '@mui/material/styles';
import { accentHover, focusOutline, focusRing, Section } from '../../components/page-primitives';
import { accentHover, focusOutline, focusRing } from '../../components/page-primitives';
import { useSearch, SEARCH_DEBOUNCE_MS } from '../../hooks/use-search';
import { useSearchQuery } from '../../context/search/search-context';
import { useSearchFocus } from '../../context/search/search-focus-context';
Expand Down Expand Up @@ -223,13 +223,7 @@ export const HeroSearch: FunctionComponent<HeroSearchProps> = ({
};

return (
<Section
component='section'
sx={{
pt: { xs: '2.75rem', sm: '4.875rem' },
pb: { xs: '1.125rem', sm: '1.875rem' },
textAlign: 'center'
}}>
<Container maxWidth='xl' component='section' sx={{ textAlign: 'center' }}>
{SearchHeader && <SearchHeader />}
<Box component='form' onSubmit={handleSubmit} sx={{ maxWidth: '41.25rem', mx: 'auto' }}>
{/* The nav field claims 'vt-search' while the hero is unregistered — duplicate names abort transitions. */}
Expand Down Expand Up @@ -296,6 +290,6 @@ export const HeroSearch: FunctionComponent<HeroSearchProps> = ({
))}
</Box>
)}
</Section>
</Container>
);
};
17 changes: 10 additions & 7 deletions webui/src/pages/home/home-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
********************************************************************************/

import { FunctionComponent, useContext } from 'react';
import { Box } from '@mui/material';
import { Navigate, useSearchParams } from 'react-router-dom';
import { PageContainer } from '../../components/page-container';
import { SectionStack } from '../../components/page-primitives';
import { MainContext } from '../../context';
import { HomeSettings } from '../../page-settings';
import { ExtensionListRoutes } from '../extension-list/extension-list-routes';
Expand Down Expand Up @@ -48,11 +49,13 @@ export const HomePage: FunctionComponent = () => {
const HomeContent: FunctionComponent<{ home?: HomeSettings }> = ({ home }) => {
const { pageSettings } = useContext(MainContext);
return (
<Box component='main' sx={{ animation: 'fadeIn .25s ease' }}>
<HeroSearch searchHeader={pageSettings.elements.searchHeader} popularSearches={home?.popularSearches} />
<BrowseCategories />
<CuratedSections sections={home?.curatedSections} />
<GetInvolved heading={home?.involvement?.heading} cards={home?.involvement?.cards} />
</Box>
<PageContainer fluid component='main' sx={{ animation: 'fadeIn .25s ease' }}>
<SectionStack>
<HeroSearch searchHeader={pageSettings.elements.searchHeader} popularSearches={home?.popularSearches} />
<BrowseCategories />
<CuratedSections sections={home?.curatedSections} />
<GetInvolved heading={home?.involvement?.heading} cards={home?.involvement?.cards} />
</SectionStack>
</PageContainer>
);
};
Loading