Add new FullPage component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-12 21:38:44 +00:00
parent 85d9eb1109
commit a8ac56ed04
11 changed files with 284 additions and 12 deletions
+2 -4
View File
@@ -5,7 +5,7 @@ import addonLinks from '@storybook/addon-links';
import { definePreview } from '@storybook/react-vite';
import { useEffect } from 'react';
import { TestApiProvider } from '@backstage/test-utils';
import { Content, AlertDisplay } from '@backstage/core-components';
import { AlertDisplay } from '@backstage/core-components';
import { apis } from './support/apis';
import { useGlobals } from 'storybook/preview-api';
import { UnifiedThemeProvider, themes } from '@backstage/theme';
@@ -146,9 +146,7 @@ export default definePreview({
{/* @ts-ignore */}
<TestApiProvider apis={apis}>
<AlertDisplay />
<Content>
<Story />
</Content>
<Story />
</TestApiProvider>
</UnifiedThemeProvider>
);
@@ -0,0 +1,24 @@
/*
* Copyright 2025 The Backstage Authors
*
* 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.
*/
@layer tokens, base, components, utilities;
@layer components {
.bui-FullPage {
height: calc(100dvh - var(--bui-header-height, 0px));
overflow-y: auto;
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2025 The Backstage Authors
*
* 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 preview from '../../../../../.storybook/preview';
import type { StoryFn } from '@storybook/react-vite';
import { FullPage } from './FullPage';
import { Header } from '../Header';
import { Container } from '../Container';
import { Text } from '../Text';
import type { HeaderTab } from '../Header/types';
import { MemoryRouter } from 'react-router-dom';
const meta = preview.meta({
title: 'Backstage UI/FullPage',
component: FullPage,
parameters: {
layout: 'fullscreen',
},
});
const withRouter = (Story: StoryFn) => (
<MemoryRouter>
<Story />
</MemoryRouter>
);
const tabs: HeaderTab[] = [
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
{ id: 'tracks', label: 'Tracks', href: '/tracks' },
{ id: 'campaigns', label: 'Campaigns', href: '/campaigns' },
];
const paragraphs = Array.from({ length: 20 }, (_, i) => (
<Text as="p" key={i}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames
ac turpis egestas. Sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua.
</Text>
));
export const Default = meta.story({
decorators: [withRouter],
render: () => (
<>
<Header title="My Plugin" />
<FullPage style={{ backgroundColor: '#c3f0ff' }}>
<Container>
<Text as="p">
This content fills the remaining viewport height below the Header.
</Text>
</Container>
</FullPage>
</>
),
});
export const WithScrollableContent = meta.story({
decorators: [withRouter],
render: () => (
<>
<Header title="My Plugin" />
<FullPage>
<Container>
<Text as="h2" variant="title-medium">
Scrollable Content
</Text>
<Text as="p">
The content below scrolls independently while the Header stays
pinned at the top.
</Text>
{paragraphs}
</Container>
</FullPage>
</>
),
});
export const WithTabs = meta.story({
decorators: [withRouter],
render: () => (
<>
<Header title="My Plugin" tabs={tabs} />
<FullPage>
<Container>
<Text as="p">
The FullPage height adjusts automatically when the Header includes
tabs, thanks to the ResizeObserver measuring the Header's actual
height.
</Text>
{paragraphs}
</Container>
</FullPage>
</>
),
});
@@ -0,0 +1,45 @@
/*
* Copyright 2025 The Backstage Authors
*
* 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 { forwardRef } from 'react';
import type { FullPageProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { FullPageDefinition } from './definition';
import styles from './FullPage.module.css';
import clsx from 'clsx';
/**
* A component that fills the remaining viewport height below the Header.
*
* The FullPage component consumes the `--bui-header-height` CSS custom property
* set by the Header component to calculate its height as
* `calc(100dvh - var(--bui-header-height, 0px))`. Content inside the FullPage
* scrolls independently while the Header stays visible.
*
* @public
*/
export const FullPage = forwardRef<HTMLElement, FullPageProps>((props, ref) => {
const { classNames, cleanedProps } = useStyles(FullPageDefinition, props);
const { className, ...rest } = cleanedProps;
return (
<main
ref={ref}
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
/>
);
});
@@ -0,0 +1,27 @@
/*
* Copyright 2025 The Backstage Authors
*
* 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 type { ComponentDefinition } from '../../types';
/**
* Component definition for FullPage
* @public
*/
export const FullPageDefinition = {
classNames: {
root: 'bui-FullPage',
},
} as const satisfies ComponentDefinition;
@@ -0,0 +1,19 @@
/*
* Copyright 2025 The Backstage Authors
*
* 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.
*/
export { FullPage } from './FullPage';
export { FullPageDefinition } from './definition';
export type { FullPageProps } from './types';
@@ -0,0 +1,22 @@
/*
* Copyright 2025 The Backstage Authors
*
* 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.
*/
/**
* Props for the FullPage component.
*
* @public
*/
export interface FullPageProps extends React.ComponentPropsWithoutRef<'main'> {}
@@ -17,9 +17,11 @@
@layer tokens, base, components, utilities;
@layer components {
.bui-HeaderToolbar {
margin-bottom: var(--bui-space-6);
.bui-Header {
display: block;
}
.bui-HeaderToolbar {
&::before {
content: '';
position: absolute;
@@ -30,10 +32,6 @@
background-color: var(--bui-bg-neutral-0);
z-index: 0;
}
&[data-has-tabs='true'] {
margin-bottom: 0;
}
}
.bui-HeaderToolbarWrapper {
+29 -2
View File
@@ -20,6 +20,7 @@ import { Tabs, TabList, Tab } from '../Tabs';
import { useStyles } from '../../hooks/useStyles';
import { HeaderDefinition } from './definition';
import { type NavigateOptions } from 'react-router-dom';
import { useRef, useEffect } from 'react';
import styles from './Header.module.css';
import clsx from 'clsx';
@@ -47,9 +48,35 @@ export const Header = (props: HeaderProps) => {
} = cleanedProps;
const hasTabs = tabs && tabs.length > 0;
const headerRef = useRef<HTMLElement>(null);
useEffect(() => {
const el = headerRef.current;
if (!el) return undefined;
const updateHeight = () => {
const height = el.offsetHeight;
document.documentElement.style.setProperty(
'--bui-header-height',
`${height}px`,
);
};
const observer = new ResizeObserver(updateHeight);
observer.observe(el);
updateHeight();
return () => {
observer.disconnect();
document.documentElement.style.removeProperty('--bui-header-height');
};
}, []);
return (
<>
<header
ref={headerRef}
className={clsx(classNames.root, styles[classNames.root])}
>
<HeaderToolbar
icon={icon}
title={title}
@@ -81,6 +108,6 @@ export const Header = (props: HeaderProps) => {
</Tabs>
</div>
)}
</>
</header>
);
};
@@ -22,6 +22,7 @@ import type { ComponentDefinition } from '../../types';
*/
export const HeaderDefinition = {
classNames: {
root: 'bui-Header',
toolbar: 'bui-HeaderToolbar',
toolbarWrapper: 'bui-HeaderToolbarWrapper',
toolbarContent: 'bui-HeaderToolbarContent',
+1
View File
@@ -25,6 +25,7 @@ export * from './components/Box';
export * from './components/Grid';
export * from './components/Flex';
export * from './components/Container';
export * from './components/FullPage';
// UI components
export * from './components/Accordion';