From b404346555f77f76a16b90d0b46d5d50b217f3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20G=C3=B3mez?= Date: Thu, 23 Jul 2026 15:45:51 +0200 Subject: [PATCH 1/3] fix: normalize section margins In the restyle PR we added a homepage with some sections, those sections are basically fully customizable, the only thing we did not take into account was that in order to allow positioning sections on a different order the sections should not hard-code the margins. This patch fixes that by using an styled parent component that sets different margin top's to it's children. --- webui/src/components/page-primitives.tsx | 20 ++++++++++++++++++++ webui/src/pages/home/browse-categories.tsx | 2 +- webui/src/pages/home/curated-sections.tsx | 2 +- webui/src/pages/home/get-involved.tsx | 2 +- webui/src/pages/home/hero-search.tsx | 8 +------- webui/src/pages/home/home-page.tsx | 8 ++++---- 6 files changed, 28 insertions(+), 14 deletions(-) diff --git a/webui/src/components/page-primitives.tsx b/webui/src/components/page-primitives.tsx index 27a4101c9..daa64d2d5 100644 --- a/webui/src/components/page-primitives.tsx +++ b/webui/src/components/page-primitives.tsx @@ -30,6 +30,26 @@ export const Section = styled(Box)(({ theme }) => ({ } })); +/** + * Vertical stack of page `Section`s that owns all vertical rhythm: a top offset + * below the nav plus a single, normalized gap between sections. Sections carry no + * spacing of their own, so a custom home can cherry-pick and reorder them — and + * whichever lands first always gets the top offset, never a leading gap. The owl + * selector skips the first (and any null) child. + */ +export const SectionStack = styled(Box)(({ theme }) => ({ + paddingTop: '4.875rem', + [theme.breakpoints.down('sm')]: { + paddingTop: '2.75rem' + }, + '& > * + *': { + marginTop: '4.5rem', + [theme.breakpoints.down('sm')]: { + marginTop: '3rem' + } + } +})); + /** Small uppercase label used to head sections, columns and sidebars. */ export const Eyebrow = styled(Typography)(({ theme }) => ({ fontSize: '0.75rem', diff --git a/webui/src/pages/home/browse-categories.tsx b/webui/src/pages/home/browse-categories.tsx index c03f3de3b..30a2255a9 100644 --- a/webui/src/pages/home/browse-categories.tsx +++ b/webui/src/pages/home/browse-categories.tsx @@ -38,7 +38,7 @@ export const BrowseCategories: FunctionComponent = props return null; } return ( -
+
Browse by category = props => return null; } return ( -
+
= ({ heading, card return null; } return ( -
+
{heading ?? 'Get Involved'} diff --git a/webui/src/pages/home/hero-search.tsx b/webui/src/pages/home/hero-search.tsx index 775289258..d5ba52692 100644 --- a/webui/src/pages/home/hero-search.tsx +++ b/webui/src/pages/home/hero-search.tsx @@ -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. */} diff --git a/webui/src/pages/home/home-page.tsx b/webui/src/pages/home/home-page.tsx index 3a1579912..311acff1f 100644 --- a/webui/src/pages/home/home-page.tsx +++ b/webui/src/pages/home/home-page.tsx @@ -12,8 +12,8 @@ ********************************************************************************/ import { FunctionComponent, useContext } from 'react'; -import { Box } from '@mui/material'; import { Navigate, useSearchParams } from 'react-router-dom'; +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 +48,11 @@ export const HomePage: FunctionComponent = () => { const HomeContent: FunctionComponent<{ home?: HomeSettings }> = ({ home }) => { const { pageSettings } = useContext(MainContext); return ( - + - + - + ); }; From 05193c7a5af11bd52a1c85d4f4b5e15d2c64a75d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20G=C3=B3mez?= Date: Thu, 23 Jul 2026 18:02:02 +0200 Subject: [PATCH 2/3] refactor: standarising page containers and margins After the refresh we introduced a section component that was hardcoding the max width, this patch moves that back to the theme config so that we can use MUI's container, and at the same time I'm creating a wrapper of the container meant for page layouts, this allows pages to consistently have the same padding top + bottom without leaving that to the layout and loosing the ability to flus it out on some cases, like for the search page, that we don't wan't any top margin since the category pills should be just under the nav to look good. --- webui/src/components/page-container.tsx | 50 ++++++++++++ webui/src/components/page-primitives.tsx | 28 +------ webui/src/default/theme.tsx | 2 +- webui/src/index.ts | 1 + webui/src/layout/app-footer.tsx | 14 ++-- webui/src/layout/app-layout.tsx | 4 +- webui/src/not-found.tsx | 7 +- .../extension-detail/extension-detail.tsx | 10 +-- webui/src/pages/home/browse-categories.tsx | 8 +- webui/src/pages/home/curated-sections.tsx | 7 +- webui/src/pages/home/get-involved.tsx | 8 +- webui/src/pages/home/hero-search.tsx | 8 +- webui/src/pages/home/home-page.tsx | 15 ++-- .../namespace-detail/namespace-detail.tsx | 11 ++- webui/src/pages/search/search-page.tsx | 11 ++- webui/src/pages/user/user-settings.tsx | 77 +++++++++---------- 16 files changed, 142 insertions(+), 119 deletions(-) create mode 100644 webui/src/components/page-container.tsx 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 daa64d2d5..06af6a71a 100644 --- a/webui/src/components/page-primitives.tsx +++ b/webui/src/components/page-primitives.tsx @@ -14,34 +14,8 @@ 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' - } -})); - -/** - * Vertical stack of page `Section`s that owns all vertical rhythm: a top offset - * below the nav plus a single, normalized gap between sections. Sections carry no - * spacing of their own, so a custom home can cherry-pick and reorder them — and - * whichever lands first always gets the top offset, never a leading gap. The owl - * selector skips the first (and any null) child. - */ +/** Normalized gap between stacked sections; the owl selector skips the first (and any null) child. */ export const SectionStack = styled(Box)(({ theme }) => ({ - paddingTop: '4.875rem', - [theme.breakpoints.down('sm')]: { - paddingTop: '2.75rem' - }, '& > * + *': { marginTop: '4.5rem', [theme.breakpoints.down('sm')]: { 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 30a2255a9..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 130f223cd..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 c7fb4b437..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 d5ba52692..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,7 +223,7 @@ export const HeroSearch: FunctionComponent = ({ }; return ( -
+ {SearchHeader && } {/* The nav field claims 'vt-search' while the hero is unregistered — duplicate names abort transitions. */} @@ -290,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 311acff1f..5eabd215a 100644 --- a/webui/src/pages/home/home-page.tsx +++ b/webui/src/pages/home/home-page.tsx @@ -13,6 +13,7 @@ import { FunctionComponent, useContext } from 'react'; 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'; @@ -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)} - - + + ); }; From bfdddfdcce2c19861bbec69cf2b376b5013cba03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20G=C3=B3mez?= Date: Thu, 23 Jul 2026 18:10:07 +0200 Subject: [PATCH 3/3] chore: relaxing section stack margins --- webui/src/components/page-primitives.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webui/src/components/page-primitives.tsx b/webui/src/components/page-primitives.tsx index 06af6a71a..d2a31cbd8 100644 --- a/webui/src/components/page-primitives.tsx +++ b/webui/src/components/page-primitives.tsx @@ -17,9 +17,9 @@ import { alpha, styled, Theme } from '@mui/material/styles'; /** Normalized gap between stacked sections; the owl selector skips the first (and any null) child. */ export const SectionStack = styled(Box)(({ theme }) => ({ '& > * + *': { - marginTop: '4.5rem', + marginTop: '2.5rem', [theme.breakpoints.down('sm')]: { - marginTop: '3rem' + marginTop: '1.5rem' } } }));