diff --git a/webui/src/components/page-container.tsx b/webui/src/components/page-container.tsx new file mode 100644 index 000000000..26fbda2b1 --- /dev/null +++ b/webui/src/components/page-container.tsx @@ -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(function PageContainer( + { fluid, flushTop, flushBottom, sx, ...props }, + ref +) { + return ( + + ); +}); diff --git a/webui/src/components/page-primitives.tsx b/webui/src/components/page-primitives.tsx index 27a4101c9..d2a31cbd8 100644 --- a/webui/src/components/page-primitives.tsx +++ b/webui/src/components/page-primitives.tsx @@ -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' + } } })); diff --git a/webui/src/default/theme.tsx b/webui/src/default/theme.tsx index a04365e2c..eff2084e5 100644 --- a/webui/src/default/theme.tsx +++ b/webui/src/default/theme.tsx @@ -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: { diff --git a/webui/src/index.ts b/webui/src/index.ts index ea90f90c1..3834d0014 100644 --- a/webui/src/index.ts +++ b/webui/src/index.ts @@ -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'; diff --git a/webui/src/layout/app-footer.tsx b/webui/src/layout/app-footer.tsx index 5f598ba4c..9630142fe 100644 --- a/webui/src/layout/app-footer.tsx +++ b/webui/src/layout/app-footer.tsx @@ -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'; @@ -131,7 +131,8 @@ const StructuredFooter: FunctionComponent = ({ footer, ve component='footer' sx={{ mt: 'auto', borderTop: '1px solid', borderColor: 'divider', bgcolor: 'background.paper' }}> {footer && (footer.brand || footer.columns) && ( -
= ({ footer, ve ))} -
+
)} -
= ({ footer, ve )} -
+ ); diff --git a/webui/src/layout/app-layout.tsx b/webui/src/layout/app-layout.tsx index 1c2eb70d5..9a6fc1048 100644 --- a/webui/src/layout/app-layout.tsx +++ b/webui/src/layout/app-layout.tsx @@ -109,8 +109,8 @@ const AppLayoutContent: FunctionComponent = 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 . + pb: legacyFooterHeight ? `${legacyFooterHeight + 24}px` : 0 }}> diff --git a/webui/src/not-found.tsx b/webui/src/not-found.tsx index cdaf42662..781a8727d 100644 --- a/webui/src/not-found.tsx +++ b/webui/src/not-found.tsx @@ -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 ( - + Oooups...this is a 404 page. - + ); }; diff --git a/webui/src/pages/extension-detail/extension-detail.tsx b/webui/src/pages/extension-detail/extension-detail.tsx index 1693a5a0e..be40ddc5d 100644 --- a/webui/src/pages/extension-detail/extension-detail.tsx +++ b/webui/src/pages/extension-detail/extension-detail.tsx @@ -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. @@ -505,7 +506,7 @@ export const ExtensionDetail: FunctionComponent = () => { {extension && ( <> - + { state={{ preserveScroll: true }} /> - {/* 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. */} - + { /> - + )} {error && ( diff --git a/webui/src/pages/home/browse-categories.tsx b/webui/src/pages/home/browse-categories.tsx index c03f3de3b..ddb9b4029 100644 --- a/webui/src/pages/home/browse-categories.tsx +++ b/webui/src/pages/home/browse-categories.tsx @@ -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'; @@ -38,7 +38,7 @@ export const BrowseCategories: FunctionComponent = props return null; } return ( -
+ Browse by category = props /> ))} -
+ ); }; diff --git a/webui/src/pages/home/curated-sections.tsx b/webui/src/pages/home/curated-sections.tsx index 12fe505d9..f65bc4a1d 100644 --- a/webui/src/pages/home/curated-sections.tsx +++ b/webui/src/pages/home/curated-sections.tsx @@ -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'; @@ -38,7 +37,7 @@ export const CuratedSections: FunctionComponent = props => return null; } return ( -
+ = props => /> ))} -
+ ); })} diff --git a/webui/src/pages/home/get-involved.tsx b/webui/src/pages/home/get-involved.tsx index d65c5cdfb..c6f451017 100644 --- a/webui/src/pages/home/get-involved.tsx +++ b/webui/src/pages/home/get-involved.tsx @@ -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, @@ -74,7 +74,7 @@ export const GetInvolved: FunctionComponent = ({ heading, card return null; } return ( -
+ {heading ?? 'Get Involved'} @@ -120,6 +120,6 @@ export const GetInvolved: FunctionComponent = ({ heading, card ); })} -
+ ); }; diff --git a/webui/src/pages/home/hero-search.tsx b/webui/src/pages/home/hero-search.tsx index 775289258..e07015f8e 100644 --- a/webui/src/pages/home/hero-search.tsx +++ b/webui/src/pages/home/hero-search.tsx @@ -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'; @@ -223,13 +223,7 @@ export const HeroSearch: FunctionComponent = ({ }; return ( -
+ {SearchHeader && } {/* The nav field claims 'vt-search' while the hero is unregistered — duplicate names abort transitions. */} @@ -296,6 +290,6 @@ export const HeroSearch: FunctionComponent = ({ ))} )} -
+ ); }; diff --git a/webui/src/pages/home/home-page.tsx b/webui/src/pages/home/home-page.tsx index 3a1579912..5eabd215a 100644 --- a/webui/src/pages/home/home-page.tsx +++ b/webui/src/pages/home/home-page.tsx @@ -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'; @@ -48,11 +49,13 @@ export const HomePage: FunctionComponent = () => { const HomeContent: FunctionComponent<{ home?: HomeSettings }> = ({ home }) => { const { pageSettings } = useContext(MainContext); return ( - - - - - - + + + + + + + + ); }; diff --git a/webui/src/pages/namespace-detail/namespace-detail.tsx b/webui/src/pages/namespace-detail/namespace-detail.tsx index fec8f1c0c..bd2c095ed 100644 --- a/webui/src/pages/namespace-detail/namespace-detail.tsx +++ b/webui/src/pages/namespace-detail/namespace-detail.tsx @@ -9,7 +9,7 @@ ********************************************************************************/ import { FunctionComponent, ReactNode, useContext, useEffect, useState, useRef } from 'react'; -import { Typography, Box, Link, Divider } from '@mui/material'; +import { Typography, Box, Container, Link, Divider } from '@mui/material'; import GitHubIcon from '@mui/icons-material/GitHub'; import LinkedInIcon from '@mui/icons-material/LinkedIn'; import TwitterIcon from '@mui/icons-material/Twitter'; @@ -17,7 +17,6 @@ import { useParams } from 'react-router-dom'; import { ExtensionCard } from '../../components/extension-card'; import { MainContext } from '../../context'; import { DelayedLoadIndicator } from '../../components/delayed-load-indicator'; -import { Section } from '../../components/page-primitives'; import { NamespaceDetails, isError, UrlString } from '../../extension-registry-types'; export const NamespaceDetail: FunctionComponent = () => { @@ -111,7 +110,7 @@ export const NamespaceDetail: FunctionComponent = () => { return ( <> -
+ { ) : null} -
+
{namespaceDetails.extensions ? ( -
+ { /> ))} -
+ ) : null} ); diff --git a/webui/src/pages/search/search-page.tsx b/webui/src/pages/search/search-page.tsx index 2407bcb6c..a4ed2c870 100644 --- a/webui/src/pages/search/search-page.tsx +++ b/webui/src/pages/search/search-page.tsx @@ -19,7 +19,8 @@ import { ExtensionList } from '../../components/extension-list'; import { CATEGORY_ICONS, DefaultCategoryIcon } from '../../components/categories'; import { CategoryPill } from '../../components/category-pill'; import { CategoryListItem } from '../../components/category-list-item'; -import { Eyebrow, Section } from '../../components/page-primitives'; +import { Eyebrow } from '../../components/page-primitives'; +import { PageContainer } from '../../components/page-container'; import { NAVBAR_HEIGHT } from '../../default/theme'; import { useSearch } from '../../hooks/use-search'; import { useCategories } from '../../components/categories'; @@ -33,10 +34,8 @@ export const SearchPage: FunctionComponent = () => { const [resultNumber, setResultNumber] = useState(0); return ( -
+ // flushTop: hug the nav; keep the default footer gap. + {/* Mobile category pills — outside the flex row so negative-margin bleed isn't clipped */} {categories.length > 0 && ( { /> -
+ ); }; diff --git a/webui/src/pages/user/user-settings.tsx b/webui/src/pages/user/user-settings.tsx index f9ef62987..877f0541b 100644 --- a/webui/src/pages/user/user-settings.tsx +++ b/webui/src/pages/user/user-settings.tsx @@ -10,8 +10,9 @@ import { FunctionComponent, ReactNode, useContext } from 'react'; import { Helmet } from 'react-helmet-async'; -import { Grid, Container, Box, Typography, Link } from '@mui/material'; +import { Grid, Box, Typography, Link } from '@mui/material'; import { useParams } from 'react-router-dom'; +import { PageContainer } from '../../components/page-container'; import { DelayedLoadIndicator } from '../../components/delayed-load-indicator'; import { UserSettingTabs } from './user-setting-tabs'; import { UserSettingsTokens } from './user-settings-tokens'; @@ -56,52 +57,48 @@ export const UserSettings: FunctionComponent = props => { if (!user) { return loginProviders ? ( - - - Not Logged In - - - Please{' '} - { - return ( - - log in - - ); - }} - />{' '} - to access your account settings. - - + + Not Logged In + + + Please{' '} + { + return ( + + log in + + ); + }} + />{' '} + to access your account settings. + - + ) : null; } return ( - - + + + + + - - - - - {renderTab(user, tab, namespace, extension)} - + item + sx={{ + pt: { xs: 0, sm: 0, md: 0, lg: '.5rem', xl: '.5rem' }, + pl: { xs: 0, sm: 0, md: 0, lg: '3rem', xl: '3rem' }, + flex: { xs: 'none', sm: 'none', md: 'none', lg: '1', xl: '1' }, + width: { xs: '100%', sm: '100%', md: '100%', lg: 'auto', xl: 'auto' } + }}> + {renderTab(user, tab, namespace, extension)} - - + + ); };