diff --git a/workspaces/theme/.changeset/calm-panel-scroll.md b/workspaces/theme/.changeset/calm-panel-scroll.md
new file mode 100644
index 00000000000..d66ec5fd971
--- /dev/null
+++ b/workspaces/theme/.changeset/calm-panel-scroll.md
@@ -0,0 +1,5 @@
+---
+'@red-hat-developer-hub/backstage-plugin-theme': minor
+---
+
+Replace PageMainContainer with a CSS-only PatternFly page-inset: BackstageSidebarPage is the sole scrollport, clipped with `clip-path` so the scrollbar follows the rounded well (no extra DOM wrapper).
diff --git a/workspaces/theme/packages/app-legacy/src/components/Root/Root.tsx b/workspaces/theme/packages/app-legacy/src/components/Root/Root.tsx
index 19bebd7b903..b4b46d5188e 100644
--- a/workspaces/theme/packages/app-legacy/src/components/Root/Root.tsx
+++ b/workspaces/theme/packages/app-legacy/src/components/Root/Root.tsx
@@ -46,7 +46,6 @@ import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
import {
LogoFull,
LogoIcon,
- PageMainContainer,
} from '@red-hat-developer-hub/backstage-plugin-theme/legacy';
const SidebarLogo = () => {
@@ -122,6 +121,6 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
- {children}
+ {children}
);
diff --git a/workspaces/theme/packages/app/src/App.tsx b/workspaces/theme/packages/app/src/App.tsx
index 4228ee103fd..21e9c6a4363 100644
--- a/workspaces/theme/packages/app/src/App.tsx
+++ b/workspaces/theme/packages/app/src/App.tsx
@@ -10,13 +10,12 @@
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
+ * See the License for the permissions and limitations under the License.
*/
import { createApp } from '@backstage/frontend-defaults';
-import { appLayoutModule, navModule } from './modules';
+import { navModule } from './modules';
export default createApp({
- features: [appLayoutModule, navModule],
+ features: [navModule],
});
diff --git a/workspaces/theme/packages/app/src/modules/index.ts b/workspaces/theme/packages/app/src/modules/index.ts
index 04ebc328481..1e921ce4d04 100644
--- a/workspaces/theme/packages/app/src/modules/index.ts
+++ b/workspaces/theme/packages/app/src/modules/index.ts
@@ -13,7 +13,6 @@
* See the License for the permissions and limitations under the License.
*/
-import { appLayoutModule } from './layout';
import { navModule } from './nav';
-export { appLayoutModule, navModule };
+export { navModule };
diff --git a/workspaces/theme/packages/app/src/modules/layout/index.tsx b/workspaces/theme/packages/app/src/modules/layout/index.tsx
deleted file mode 100644
index d2c8926eb0c..00000000000
--- a/workspaces/theme/packages/app/src/modules/layout/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright Red Hat, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the permissions and limitations under the License.
- */
-
-import { SidebarPage } from '@backstage/core-components';
-import {
- coreExtensionData,
- createExtension,
- createExtensionInput,
- createFrontendModule,
-} from '@backstage/frontend-plugin-api';
-import { PageMainContainer } from '@red-hat-developer-hub/backstage-plugin-theme';
-
-/**
- * Replaces the default `app/layout` so all non-sidebar content (PluginHeader,
- * Containers, routes) lives under one PatternFly-aligned main well.
- */
-const AppLayout = createExtension({
- name: 'layout',
- attachTo: { id: 'app/root', input: 'children' },
- inputs: {
- nav: createExtensionInput([coreExtensionData.reactElement], {
- singleton: true,
- }),
- content: createExtensionInput([coreExtensionData.reactElement], {
- singleton: true,
- }),
- },
- output: [coreExtensionData.reactElement],
- factory: ({ inputs }) => [
- coreExtensionData.reactElement(
-
- {inputs.nav.get(coreExtensionData.reactElement)}
-
- {inputs.content.get(coreExtensionData.reactElement)}
-
- ,
- ),
- ],
-});
-
-export const appLayoutModule = createFrontendModule({
- pluginId: 'app',
- extensions: [AppLayout],
-});
diff --git a/workspaces/theme/plugins/bcc-test/report-alpha.api.md b/workspaces/theme/plugins/bcc-test/report-alpha.api.md
index 9d3a88fd224..e14d404a930 100644
--- a/workspaces/theme/plugins/bcc-test/report-alpha.api.md
+++ b/workspaces/theme/plugins/bcc-test/report-alpha.api.md
@@ -32,7 +32,6 @@ const _default: OverridableFrontendPlugin<
title?: string | undefined;
};
output:
- | ExtensionDataRef
| ExtensionDataRef
| ExtensionDataRef<
RouteRef,
@@ -41,6 +40,7 @@ const _default: OverridableFrontendPlugin<
optional: true;
}
>
+ | ExtensionDataRef
| ExtensionDataRef<
string,
'core.title',
diff --git a/workspaces/theme/plugins/bui-test/report-alpha.api.md b/workspaces/theme/plugins/bui-test/report-alpha.api.md
index cef91e307bc..a6bdd9a6bac 100644
--- a/workspaces/theme/plugins/bui-test/report-alpha.api.md
+++ b/workspaces/theme/plugins/bui-test/report-alpha.api.md
@@ -32,7 +32,6 @@ const _default: OverridableFrontendPlugin<
title?: string | undefined;
};
output:
- | ExtensionDataRef
| ExtensionDataRef
| ExtensionDataRef<
RouteRef,
@@ -41,6 +40,7 @@ const _default: OverridableFrontendPlugin<
optional: true;
}
>
+ | ExtensionDataRef
| ExtensionDataRef<
string,
'core.title',
diff --git a/workspaces/theme/plugins/mui4-test/report-alpha.api.md b/workspaces/theme/plugins/mui4-test/report-alpha.api.md
index 7caef2f3742..44cb74f5b73 100644
--- a/workspaces/theme/plugins/mui4-test/report-alpha.api.md
+++ b/workspaces/theme/plugins/mui4-test/report-alpha.api.md
@@ -32,7 +32,6 @@ const plugin: OverridableFrontendPlugin<
title?: string | undefined;
};
output:
- | ExtensionDataRef
| ExtensionDataRef
| ExtensionDataRef<
RouteRef,
@@ -41,6 +40,7 @@ const plugin: OverridableFrontendPlugin<
optional: true;
}
>
+ | ExtensionDataRef
| ExtensionDataRef<
string,
'core.title',
diff --git a/workspaces/theme/plugins/mui5-test/report-alpha.api.md b/workspaces/theme/plugins/mui5-test/report-alpha.api.md
index f6ddea6d7fe..b20c2139ac1 100644
--- a/workspaces/theme/plugins/mui5-test/report-alpha.api.md
+++ b/workspaces/theme/plugins/mui5-test/report-alpha.api.md
@@ -32,7 +32,6 @@ const _default: OverridableFrontendPlugin<
title?: string | undefined;
};
output:
- | ExtensionDataRef
| ExtensionDataRef
| ExtensionDataRef<
RouteRef,
@@ -41,6 +40,7 @@ const _default: OverridableFrontendPlugin<
optional: true;
}
>
+ | ExtensionDataRef
| ExtensionDataRef<
string,
'core.title',
diff --git a/workspaces/theme/plugins/theme/report-legacy.api.md b/workspaces/theme/plugins/theme/report-legacy.api.md
index 200ba21edce..b16baa8043a 100644
--- a/workspaces/theme/plugins/theme/report-legacy.api.md
+++ b/workspaces/theme/plugins/theme/report-legacy.api.md
@@ -7,7 +7,6 @@ import { AppTheme } from '@backstage/core-plugin-api';
import { default as DarkIcon } from '@mui/icons-material/Brightness2Rounded';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { default as LightIcon } from '@mui/icons-material/WbSunnyRounded';
-import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
import { Theme } from '@mui/material';
import { UnifiedThemeOptions } from '@backstage/theme';
@@ -91,9 +90,6 @@ export const LogoFull: (props: React.ComponentProps<'svg'>) => JSX_2.Element;
// @public (undocumented)
export const LogoIcon: (props: React.ComponentProps<'svg'>) => JSX_2.Element;
-// @public (undocumented)
-export const PageMainContainer: (input: PropsWithChildren) => JSX_2.Element;
-
// @public (undocumented)
export interface RHDHThemePalette {
// (undocumented)
diff --git a/workspaces/theme/plugins/theme/report.api.md b/workspaces/theme/plugins/theme/report.api.md
index 766258c7607..7ab10c77010 100644
--- a/workspaces/theme/plugins/theme/report.api.md
+++ b/workspaces/theme/plugins/theme/report.api.md
@@ -5,7 +5,6 @@
```ts
import { FrontendModule } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react/jsx-runtime';
-import { PropsWithChildren } from 'react';
// @public (undocumented)
export const LogoFull: (props: React.ComponentProps<'svg'>) => JSX_2.Element;
@@ -13,9 +12,6 @@ export const LogoFull: (props: React.ComponentProps<'svg'>) => JSX_2.Element;
// @public (undocumented)
export const LogoIcon: (props: React.ComponentProps<'svg'>) => JSX_2.Element;
-// @public (undocumented)
-export const PageMainContainer: (input: PropsWithChildren) => JSX_2.Element;
-
// @public
const rhdhThemeModule: FrontendModule;
export default rhdhThemeModule;
diff --git a/workspaces/theme/plugins/theme/src/assets/bui/component-overrides.css b/workspaces/theme/plugins/theme/src/assets/bui/component-overrides.css
index 20f30ee7ced..f5d56badae3 100644
--- a/workspaces/theme/plugins/theme/src/assets/bui/component-overrides.css
+++ b/workspaces/theme/plugins/theme/src/assets/bui/component-overrides.css
@@ -619,6 +619,26 @@ body[data-theme-name='dark-customized'][data-theme-mode='dark'] {
#f2f2f2
);
}
+ /*
+ * Hover/focus parity: BUI only reveals the sort chevron on .bui-TableHead:hover
+ * (or when already sorted). Show it on keyboard focus too so mouse and keyboard
+ * users get the same sortable affordance.
+ *
+ * Keyboard note (React Aria / BUI table): Tab enters or leaves the table as a
+ * single stop. To move between sortable headers while one is focused, use the
+ * arrow keys — not Tab.
+ */
+ .bui-TableHead:focus-visible .bui-TableHeadSortButton,
+ .bui-TableHead[data-focused] .bui-TableHeadSortButton,
+ .bui-TableHead[data-focus-visible] .bui-TableHeadSortButton {
+ opacity: 1;
+ }
+ .bui-TableHead:focus-visible,
+ .bui-TableHead[data-focused],
+ .bui-TableHead[data-focus-visible] {
+ outline: 2px solid var(--bui-ring, var(--bui-bg-solid));
+ outline-offset: -2px;
+ }
.bui-TableHead[data-sort-direction='ascending'] .bui-TableHeadSortButton,
.bui-TableHead[data-sort-direction='descending'] .bui-TableHeadSortButton {
color: var(--pf-t--global--color--brand--clicked, #004d99);
@@ -832,10 +852,10 @@ body[data-theme-name='dark-customized'][data-theme-mode='dark'] {
/*
* Page inset / rounded content well:
- * Prefer a PatternFly-style main container (`RHDHPageMainContainer` /
- * `#rhdh-page-main-container`) beside the sidebar — see createComponents.ts.
- * Do not clip/round BackstageSidebarPage-root itself; that fights scrolling
- * when Header + Containers are loose siblings of nav (BUI pages).
+ * BackstageSidebarPage is the sole scrollport. createComponents.ts applies
+ * margin inset + clip-path, sticky left-edge corner masks, and
+ * overscroll-behavior: none — for both classic and fragmented BUI
+ * Header + Container siblings; no extra DOM wrapper.
*/
}
diff --git a/workspaces/theme/plugins/theme/src/components/PageMainContainer.tsx b/workspaces/theme/plugins/theme/src/components/PageMainContainer.tsx
deleted file mode 100644
index 189e2be66b5..00000000000
--- a/workspaces/theme/plugins/theme/src/components/PageMainContainer.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Copyright Red Hat, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { PropsWithChildren } from 'react';
-import Box from '@mui/material/Box';
-import { styled } from '@mui/material/styles';
-
-/**
- * PatternFly-aligned main content well beside the sidebar.
- *
- * Mirrors `.pf-v6-c-page__main-container`: everything except the sidebar
- * (headers, page body, footers) should live under this wrapper so page-inset
- * rounding and scrolling apply to one surface — not the SidebarPage root.
- *
- * Use inside `SidebarPage` as a sibling of `Sidebar` / `nav`:
- *
- * ```tsx
- *
- * ...
- * {children}
- *
- * ```
- *
- * @public
- */
-const PageMainContainerRoot = styled(Box, {
- name: 'RHDHPageMainContainer',
- slot: 'root',
-})(() => ({
- display: 'flex',
- flexDirection: 'column',
- flex: '1 1 auto',
- minWidth: 0,
- minHeight: 0,
-}));
-
-export const PageMainContainer = ({ children }: PropsWithChildren) => (
-
- {children}
-
-);
diff --git a/workspaces/theme/plugins/theme/src/components/index.ts b/workspaces/theme/plugins/theme/src/components/index.ts
index a06708d0d22..011c9dad5a7 100644
--- a/workspaces/theme/plugins/theme/src/components/index.ts
+++ b/workspaces/theme/plugins/theme/src/components/index.ts
@@ -16,5 +16,4 @@
export * from './LogoFull';
export * from './LogoIcon';
-export * from './PageMainContainer';
export * from './ThemeProvider';
diff --git a/workspaces/theme/plugins/theme/src/index.ts b/workspaces/theme/plugins/theme/src/index.ts
index b94c22633ac..8e78a1c699f 100644
--- a/workspaces/theme/plugins/theme/src/index.ts
+++ b/workspaces/theme/plugins/theme/src/index.ts
@@ -28,7 +28,7 @@ import './assets/fonts/font.min.css';
import './assets/bui/tokens.css';
import './assets/bui/component-overrides.css';
-export { LogoFull, LogoIcon, PageMainContainer } from './components';
+export { LogoFull, LogoIcon } from './components';
/**
* RHDH themes as NFS extensions (ThemeBlueprint).
diff --git a/workspaces/theme/plugins/theme/src/legacyExports.ts b/workspaces/theme/plugins/theme/src/legacyExports.ts
index fe07c07a63b..1338a9f2143 100644
--- a/workspaces/theme/plugins/theme/src/legacyExports.ts
+++ b/workspaces/theme/plugins/theme/src/legacyExports.ts
@@ -21,7 +21,7 @@ import './assets/fonts/font.min.css';
export * from './hooks';
export * from './themes';
-export { LogoFull, LogoIcon, PageMainContainer } from './components';
+export { LogoFull, LogoIcon } from './components';
export type {
BackstageThemePalette,
RHDHThemePalette,
diff --git a/workspaces/theme/plugins/theme/src/utils/createComponents.test.ts b/workspaces/theme/plugins/theme/src/utils/createComponents.test.ts
index e55fa306ee7..074c50668e4 100644
--- a/workspaces/theme/plugins/theme/src/utils/createComponents.test.ts
+++ b/workspaces/theme/plugins/theme/src/utils/createComponents.test.ts
@@ -92,13 +92,20 @@ describe('createComponents', () => {
expect(actual.BackstageSidebarPage?.styleOverrides?.root).toEqual(
expect.objectContaining({
minHeight: '100vh',
- display: 'flex',
- flexDirection: 'column',
}),
);
});
- it('stretches main with mainSectionBackgroundColor inside the page inset', () => {
+ it('paints BackstageSidebarPage with mainSectionBackgroundColor on all viewports', () => {
+ const actual = createComponents({ palette: customDarkTheme() });
+ expect(actual.BackstageSidebarPage?.styleOverrides?.root).toEqual(
+ expect.objectContaining({
+ backgroundColor: '#292929',
+ }),
+ );
+ });
+
+ it('clips the SidebarPage scrollport so the scrollbar follows the rounded well', () => {
const actual = createComponents({ palette: customDarkTheme() });
const root = actual.BackstageSidebarPage?.styleOverrides?.root as
| Record
@@ -109,35 +116,54 @@ describe('createComponents', () => {
expect(desktop).toEqual(
expect.objectContaining({
boxSizing: 'border-box',
- overflow: 'hidden',
- height: '100vh',
- maxHeight: '100vh',
+ overflowY: 'auto',
+ width: 'calc(100% - 1.5rem) !important',
+ marginTop: '1.5rem',
+ marginRight: '1.5rem',
+ marginBottom: '1.5rem',
+ marginLeft: 0,
+ height: 'calc(100vh - 2 * 1.5rem)',
+ maxHeight: 'calc(100vh - 2 * 1.5rem)',
overscrollBehavior: 'none',
- paddingTop: '1.5rem',
- paddingRight: '1.5rem',
- paddingBottom: '1.5rem',
+ borderRadius: '1rem',
+ clipPath: 'inset(0 round 1rem)',
+ }),
+ );
+ expect(desktop?.['&::before']).toEqual(
+ expect.objectContaining({
+ position: 'sticky',
+ top: 0,
+ pointerEvents: 'none',
+ zIndex: 101,
+ }),
+ );
+ expect(
+ desktop?.["& > [class*='MuiLinearProgress-root'], & > main"],
+ ).toEqual(
+ expect.objectContaining({
+ backgroundColor: '#292929',
+ borderRadius: 0,
+ overflow: 'visible',
+ }),
+ );
+ expect(desktop?.['& .fullscreen']).toEqual(
+ expect.objectContaining({
+ position: 'relative',
}),
);
expect(
desktop?.[
- "& > [class*='MuiLinearProgress-root'], & > main, & > [class*='RHDHPageMainContainer']"
+ '& .fullscreen > .MuiIconButton-root, & .fullscreen .MuiIconButton-root[class*="fullscreenButton"]'
],
).toEqual(
expect.objectContaining({
- backgroundColor: '#292929',
- borderRadius: '1rem',
- margin: 0,
- flex: '1 1 auto',
- minHeight: 0,
- overflowY: 'auto',
- overscrollBehaviorY: 'contain',
- display: 'flex',
- flexDirection: 'column',
+ top: '0.5rem !important',
+ right: '0.5rem !important',
}),
);
});
- it('makes NFS BUI main a flex column without unlocking viewport height', () => {
+ it('does not require a PageMainContainer wrapper for the content well', () => {
const actual = createComponents({ palette: customDarkTheme() });
const root = actual.BackstageSidebarPage?.styleOverrides?.root as
| Record
@@ -145,18 +171,8 @@ describe('createComponents', () => {
const desktop = root?.['@media (min-width: 600px)'] as
| Record
| undefined;
- expect(
- desktop?.[
- "& > [class*='MuiLinearProgress-root'], & > main, & > [class*='RHDHPageMainContainer']"
- ],
- ).toEqual(
- expect.objectContaining({
- display: 'flex',
- flexDirection: 'column',
- }),
- );
+ expect(JSON.stringify(desktop)).not.toContain('RHDHPageMainContainer');
});
-
it('offsets BUI dialogs below the masthead so Inspect Entity stays visible', () => {
const actual = createComponents({});
const overrides = actual.MuiCssBaseline?.styleOverrides;
@@ -174,9 +190,7 @@ describe('createComponents', () => {
| Record
| undefined;
expect(
- desktop?.[
- "& > [class*='bui-Container']:not([class*='bui-Header']), & > [class*='RHDHPageMainContainer'] [class*='bui-Container']:not([class*='bui-Header'])"
- ],
+ desktop?.["& > [class*='bui-Container']:not([class*='bui-Header'])"],
).toEqual(
expect.objectContaining({
backgroundColor: '#292929',
@@ -184,7 +198,7 @@ describe('createComponents', () => {
);
});
- it('grows BackstageContent article to fill the flex column', () => {
+ it('paints BackstageContent article with mainSectionBackgroundColor', () => {
const actual = createComponents({ palette: customDarkTheme() });
const root = actual.BackstageSidebarPage?.styleOverrides?.root as
| Record
@@ -193,12 +207,9 @@ describe('createComponents', () => {
| Record
| undefined;
expect(
- desktop?.[
- '& > article, & > [class*="BackstageContent-root"], & > [class*="RHDHPageMainContainer"] > article, & > [class*="RHDHPageMainContainer"] > [class*="BackstageContent-root"]'
- ],
+ desktop?.['& > article, & > [class*="BackstageContent-root"]'],
).toEqual(
expect.objectContaining({
- flex: 1,
backgroundColor: '#292929',
}),
);
diff --git a/workspaces/theme/plugins/theme/src/utils/createComponents.ts b/workspaces/theme/plugins/theme/src/utils/createComponents.ts
index b610b2aa611..2e36b563717 100644
--- a/workspaces/theme/plugins/theme/src/utils/createComponents.ts
+++ b/workspaces/theme/plugins/theme/src/utils/createComponents.ts
@@ -44,7 +44,6 @@ export type Components = UnifiedThemeOptions['components'] & {
CatalogReactUserListPicker?: Component;
PrivateTabIndicator?: Component;
RHDHPageWithoutFixHeight?: Component;
- RHDHPageMainContainer?: Component;
};
export const createComponents = (themeConfig: ThemeConfig): Components => {
@@ -117,6 +116,19 @@ export const createComponents = (themeConfig: ThemeConfig): Components => {
'@media (min-width: 600px)': {
height: '100%',
overflow: 'hidden',
+ // Shows through SidebarPage's page-inset margin (clip-path well).
+ backgroundColor:
+ general.pageInsetBackgroundColor ??
+ general.appBarBackgroundColor ??
+ theme.palette.background.default,
+ },
+ },
+ '#rhdh-sidebar-layout': {
+ '@media (min-width: 600px)': {
+ backgroundColor:
+ general.pageInsetBackgroundColor ??
+ general.appBarBackgroundColor ??
+ theme.palette.background.default,
},
},
'h1, h2, h3, h4, h5, h6': {
@@ -779,6 +791,10 @@ export const createComponents = (themeConfig: ThemeConfig): Components => {
'@media (min-width: 600px)': {
'#rhdh-above-sidebar-header-container:has(*) ~ #rhdh-sidebar-layout':
{
+ // Page inset is margin on SidebarPage; don't double-gap under the masthead.
+ "& [class*='BackstageSidebarPage-root']": {
+ marginTop: '0 !important',
+ },
"& main, & [class*='MuiLinearProgress-root']": {
marginTop: '0 !important',
},
@@ -813,105 +829,127 @@ export const createComponents = (themeConfig: ThemeConfig): Components => {
// height; without a min-height here the page-inset background stops
// early and body/html shows through (RHDHBUGS-3498).
minHeight: '100vh',
- // Let BUI Container's flex: 1 grow into the remaining viewport below
- // PluginHeader / Header slots (those slots set flex: none).
- display: 'flex',
- flexDirection: 'column',
- // Controls the page inset as in PF6 -- only in desktop view
+ // All viewports: paint the content well. Page-inset chrome below is
+ // desktop-only; without this, mobile falls through to body
+ // (--bui-bg-app / page-inset) instead of mainSectionBackgroundColor.
+ backgroundColor: general.mainSectionBackgroundColor,
+ // Controls the page inset as in PF6 -- only in desktop view.
+ // CSS-only: SidebarPage is the sole scrollport so BUI siblings
+ // (PluginHeader + Containers) and classic share one rounded
+ // well without an extra DOM wrapper (PageMainContainer).
+ //
+ // Scrollbar clipping: border-radius alone does not clip Chromium
+ // scrollbars. clip-path on this same scrollport does. Margin (not
+ // border) forms the page inset so the clipped edge is the well
+ // edge — the overflow/scrollbar edge — not the outer border-box.
+ //
+ // Backstage SidebarPage sets width:100%; horizontal margins would
+ // otherwise overflow and get clipped (right gutter disappears).
'@media (min-width: 600px)': {
- backgroundColor:
- general.pageInsetBackgroundColor ?? general.appBarBackgroundColor,
- // Fixed viewport shell. Inset uses padding (border-box) so in-flow
- // children still respect Backstage's paddingLeft for the sidebar.
- // Do not position the main well absolutely — that ignores drawer padding.
boxSizing: 'border-box',
- height: '100vh',
- maxHeight: '100vh',
+ // Override Backstage `width: 100%` so margin-right is not pushed
+ // off-screen by the parent overflow:hidden.
+ width: `calc(100% - ${general.pageInset}) !important`,
+ marginTop: general.pageInset,
+ marginRight: general.pageInset,
+ marginBottom: general.pageInset,
+ marginLeft: 0,
+ height: `calc(100vh - 2 * ${general.pageInset})`,
+ maxHeight: `calc(100vh - 2 * ${general.pageInset})`,
minHeight: '0 !important',
- overflow: 'hidden',
+ overflowX: 'hidden',
+ overflowY: 'auto',
+ // `contain` still allows rubber-band overscroll, which reveals an
+ // empty rounded well above the content. `none` disables that.
overscrollBehavior: 'none',
- paddingTop: general.pageInset,
- paddingRight: general.pageInset,
- paddingBottom: general.pageInset,
- // PatternFly `.pf-v6-c-page__main-container` equivalent + classic
- "& > [class*='MuiLinearProgress-root'], & > main, & > [class*='RHDHPageMainContainer']":
- {
- // Match PF border-radius; clip-path also rounds the scrollbar track.
- borderRadius: '1rem',
- clipPath: 'rect(0 100% 100% 0 round 1rem)',
- margin: 0,
- // Fill the inset well so short pages use mainSectionBackgroundColor
- // (#292929) instead of leaving a pageInset (#151515) band below content.
- backgroundColor: general.mainSectionBackgroundColor,
- // Flex-fill the padded content box; minHeight:0 enables internal scroll.
- flex: '1 1 auto',
- minHeight: 0,
- alignSelf: 'stretch',
- width: '100%',
- // Scroll only inside this well; contain so wheel/trackpad does not
- // chain to the document once you hit the top/bottom.
- overflowY: 'auto',
- overscrollBehaviorY: 'contain',
- display: 'flex',
- flexDirection: 'column',
- },
- // NFS / BUI pages use Container instead of . Match the content
- // well color (same token as BackstageContent) and rely on flex: 1
- // from BUI rather than 100vh so PluginHeader siblings are not overflowed.
- "& > [class*='bui-Container']:not([class*='bui-Header']), & > [class*='RHDHPageMainContainer'] [class*='bui-Container']:not([class*='bui-Header'])":
- {
- backgroundColor: general.mainSectionBackgroundColor,
- },
+ borderRadius: '1rem',
+ // Clips the scrollbar into the rounded well (border-radius cannot).
+ clipPath: 'inset(0 round 1rem)',
+ // Left corners: clip-path rounds the border-box (under the drawer
+ // spacer), so the visible edge after paddingLeft stays square.
+ // Sticky masks paint the curve at the content edge. Keep
+ // overscroll-behavior: none so rubber-band does not show a second well.
+ // z-index must sit above Backstage Header (z-index: 100) or MUI
+ // Page headers cover the top-left mask tile.
+ '&::before': {
+ content: '""',
+ position: 'sticky',
+ top: 0,
+ display: 'block',
+ width: '100%',
+ height: `calc(100vh - 2 * ${general.pageInset})`,
+ marginBottom: `calc(0px - (100vh - 2 * ${general.pageInset}))`,
+ pointerEvents: 'none',
+ zIndex: 101,
+ backgroundImage: [
+ `radial-gradient(circle at 100% 100%, transparent 1rem, ${
+ general.pageInsetBackgroundColor ??
+ general.appBarBackgroundColor
+ } 1.01rem)`,
+ `radial-gradient(circle at 0% 100%, transparent 1rem, ${
+ general.pageInsetBackgroundColor ??
+ general.appBarBackgroundColor
+ } 1.01rem)`,
+ `radial-gradient(circle at 100% 0%, transparent 1rem, ${
+ general.pageInsetBackgroundColor ??
+ general.appBarBackgroundColor
+ } 1.01rem)`,
+ `radial-gradient(circle at 0% 0%, transparent 1rem, ${
+ general.pageInsetBackgroundColor ??
+ general.appBarBackgroundColor
+ } 1.01rem)`,
+ ].join(', '),
+ backgroundPosition:
+ 'top left, top right, bottom left, bottom right',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: '1rem 1rem',
+ },
+ // No nested scroll/radius on children — the shell scrolls as one.
+ "& > [class*='MuiLinearProgress-root'], & > main": {
+ margin: 0,
+ borderRadius: 0,
+ clipPath: 'none',
+ backgroundColor: general.mainSectionBackgroundColor,
+ overflow: 'visible',
+ height: 'auto',
+ maxHeight: 'none',
+ },
+ "& > [class*='bui-Container']:not([class*='bui-Header'])": {
+ backgroundColor: general.mainSectionBackgroundColor,
+ },
// When a BackstagePage-root is present, the MUI page already has
// its own header; hide the sibling BUI PluginHeader to avoid duplication.
- "&:has([class*='BackstagePage-root']) > .bui-PluginHeader, &:has([class*='BackstagePage-root']) > [class*='RHDHPageMainContainer'] > .bui-PluginHeader":
- {
- display: 'none',
- },
- // Settings and other pages render BackstageContent as .
- // Grow it to fill the flex column so pageInset doesn't show as a band.
- '& > article, & > [class*="BackstageContent-root"], & > [class*="RHDHPageMainContainer"] > article, & > [class*="RHDHPageMainContainer"] > [class*="BackstageContent-root"]':
- {
- flex: 1,
- backgroundColor: general.mainSectionBackgroundColor,
- },
- // Prevent TechDocs double scrollbar: unlock the main well and let this
- // shell scroll instead (ToC has its own scrollbar).
- "&:has(> main:has([data-testid='techdocs-native-shadowroot']))": {
- overflowY: 'auto',
- overscrollBehaviorY: 'contain',
+ "&:has([class*='BackstagePage-root']) > .bui-PluginHeader": {
+ display: 'none',
+ },
+ '& > article, & > [class*="BackstageContent-root"]': {
+ backgroundColor: general.mainSectionBackgroundColor,
},
+ // TechDocs ToC has its own scrollbar — avoid forcing a nested main well.
"& > main:has([data-testid='techdocs-native-shadowroot'])": {
height: 'auto !important',
maxHeight: 'none !important',
- flex: '0 0 auto',
- borderRadius: '1rem',
marginRight: '0.5rem',
},
- // The Backstage suspense is an MUI LinearProgress that is not wrapped by
- // a `main`. Fill the padded shell like the main well.
"& > [class*='MuiLinearProgress-root']": {
- backgroundColor: general.mainSectionBackgroundColor,
"& > [class*='MuiLinearProgress-']": {
height: '0.5rem !important',
},
},
- },
- },
- },
- };
- // PatternFly `.pf-v6-c-page__main-container` — fixed inset frame beside sidebar
- components.RHDHPageMainContainer = {
- styleOverrides: {
- root: {
- '@media (min-width: 600px)': {
- // Inherits height/radius/overflow via BackstageSidebarPage
- // `> RHDHPageMainContainer` selectors; flex column stacks BUI chrome.
- display: 'flex',
- flexDirection: 'column',
- flex: '1 1 auto',
- minHeight: 0,
- minWidth: 0,
+ // DependencyGraph's IconButton is `position:absolute; right:0` but
+ // `.fullscreen` is not positioned. Our clip-path on SidebarPage
+ // becomes the absolute containing block, so the control sticks to
+ // the page-well corner. Re-contain it to the graph, then inset it
+ // slightly so the 1rem curve does not shear the icon.
+ '& .fullscreen': {
+ position: 'relative',
+ },
+ '& .fullscreen > .MuiIconButton-root, & .fullscreen .MuiIconButton-root[class*="fullscreenButton"]':
+ {
+ top: '0.5rem !important',
+ right: '0.5rem !important',
+ zIndex: 3,
+ },
},
},
},