Move the HeaderPage into its own component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-07-02 09:53:53 +01:00
parent 1a99e7a4bf
commit d06b52446b
11 changed files with 347 additions and 52 deletions
+104
View File
@@ -20,6 +20,7 @@ import { HTMLAttributes } from 'react';
import { JSX as JSX_2 } from 'react/jsx-runtime';
import { LinkProps as LinkProps_2 } from 'react-aria-components';
import { Menu as Menu_2 } from '@base-ui-components/react/menu';
import { MutableRefObject } from 'react';
import type { RadioGroupProps as RadioGroupProps_2 } from 'react-aria-components';
import type { RadioProps as RadioProps_2 } from 'react-aria-components';
import { ReactElement } from 'react';
@@ -936,6 +937,109 @@ export interface GridProps extends SpaceProps {
style?: React.CSSProperties;
}
// @public
export const Header: (props: HeaderProps) => JSX_2.Element;
// @public
export interface HeaderBreadcrumb {
// (undocumented)
href: string;
// (undocumented)
label: string;
}
// @public
export interface HeaderIndicatorsProps {
// (undocumented)
hoveredKey: string | null;
// (undocumented)
prevHoveredKey: MutableRefObject<string | null>;
// (undocumented)
tabRefs: MutableRefObject<Map<string, HTMLDivElement>>;
// (undocumented)
tabsRef: MutableRefObject<HTMLDivElement | null>;
}
// @public
export interface HeaderOption {
// (undocumented)
label: string;
// (undocumented)
onClick?: () => void;
// (undocumented)
value: string;
}
// @public
export const HeaderPage: (props: HeaderPageProps) => JSX_2.Element;
// @public
export interface HeaderPageOption {
// (undocumented)
label: string;
// (undocumented)
onClick?: () => void;
// (undocumented)
value: string;
}
// @public
export interface HeaderPageProps {
// (undocumented)
description?: string;
// (undocumented)
name?: string;
// (undocumented)
options?: HeaderPageOption[];
// (undocumented)
tabs?: HeaderPageTab[];
}
// @public
export interface HeaderPageTab {
// (undocumented)
href?: string;
// (undocumented)
label: string;
}
// @public
export interface HeaderProps {
// (undocumented)
breadcrumbs?: HeaderBreadcrumb[];
// (undocumented)
icon?: React.ReactNode;
// (undocumented)
name?: string;
// (undocumented)
options?: HeaderOption[];
// (undocumented)
tabs?: HeaderTab[];
}
// @public
export interface HeaderTab {
// (undocumented)
href?: string;
// (undocumented)
label: string;
}
// @public
export interface HeaderTabProps {
// (undocumented)
setHoveredKey: (key: string | null) => void;
// (undocumented)
setTabRef: (key: string, element: HTMLDivElement | null) => void;
// (undocumented)
tab: HeaderTab;
}
// @public
export const HeaderTabsIndicators: (
props: HeaderIndicatorsProps,
) => JSX_2.Element;
// @public (undocumented)
export const Heading: <T extends ElementType = 'h1'>(
props: HeadingProps<T> & {
@@ -16,12 +16,7 @@
import type { Meta, StoryObj, StoryFn } from '@storybook/react';
import { Header } from './Header';
import {
HeaderBreadcrumb,
HeaderOption,
HeaderProps,
HeaderTab,
} from './types';
import { HeaderBreadcrumb, HeaderOption, HeaderTab } from './types';
const meta = {
title: 'Components/Header',
@@ -75,13 +70,6 @@ const options: HeaderOption[] = [
},
];
const subNavigation: HeaderProps['subNavigation'] = {
name: 'Sub Navigation',
description: 'Sub Navigation Description',
tabs: tabs,
options: options,
};
// Extract layout decorator as a reusable constant
const layoutDecorator = [
(Story: StoryFn) => (
@@ -146,19 +134,11 @@ export const WithBreadcrumbs: Story = {
},
};
export const WithTabsAndSubNavigation: Story = {
args: {
tabs,
subNavigation,
},
};
export const WithAllComponents: Story = {
args: {
options,
tabs,
breadcrumbs,
subNavigation,
},
};
@@ -173,16 +153,3 @@ export const WithLayout: Story = {
layout: 'fullscreen',
},
};
export const WithSubNavigation: Story = {
args: {
options,
tabs,
breadcrumbs,
subNavigation,
},
decorators: layoutDecorator,
parameters: {
layout: 'fullscreen',
},
};
@@ -17,7 +17,6 @@
import type { HeaderProps } from './types';
import { HeaderToolbar } from './HeaderToolbar';
import { HeaderTabs } from './HeaderTabs';
import { HeaderSubNav } from './HeaderSubNav';
/**
* A component that renders a toolbar.
@@ -25,7 +24,7 @@ import { HeaderSubNav } from './HeaderSubNav';
* @public
*/
export const Header = (props: HeaderProps) => {
const { tabs, icon, name, options, breadcrumbs, subNavigation } = props;
const { tabs, icon, name, options, breadcrumbs } = props;
return (
<>
@@ -36,7 +35,6 @@ export const Header = (props: HeaderProps) => {
breadcrumbs={breadcrumbs}
/>
<HeaderTabs tabs={tabs} />
<HeaderSubNav subNavigation={subNavigation} />
</>
);
};
@@ -27,12 +27,6 @@ export interface HeaderProps {
tabs?: HeaderTab[];
options?: HeaderOption[];
breadcrumbs?: HeaderBreadcrumb[];
subNavigation?: {
name?: string;
description?: string;
tabs?: HeaderTab[];
options?: HeaderOption[];
};
}
/**
@@ -0,0 +1,144 @@
/*
* Copyright 2024 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 { Meta, StoryObj, StoryFn } from '@storybook/react';
import { HeaderPage } from './HeaderPage';
import { HeaderPageOption, HeaderPageTab } from './types';
const meta = {
title: 'Components/HeaderPage',
component: HeaderPage,
} satisfies Meta<typeof HeaderPage>;
export default meta;
type Story = StoryObj<typeof meta>;
const tabs: HeaderPageTab[] = [
{
label: 'Overview',
},
{
label: 'Checks',
},
{
label: 'Tracks',
},
{
label: 'Campaigns',
},
{
label: 'Integrations',
},
];
const options: HeaderPageOption[] = [
{
label: 'Settings',
value: 'settings',
},
{
label: 'Invite new members',
value: 'invite-new-members',
},
];
// Extract layout decorator as a reusable constant
const layoutDecorator = [
(Story: StoryFn) => (
<>
<div
style={{
width: '250px',
position: 'fixed',
left: 8,
top: 8,
bottom: 8,
backgroundColor: 'var(--canon-bg-surface-1',
borderRadius: 'var(--canon-radius-2)',
border: '1px solid var(--canon-border)',
zIndex: 1,
}}
/>
<div
style={{
paddingInline: '266px',
minHeight: '200vh',
}}
>
<Story />
</div>
<div
style={{
width: '250px',
position: 'fixed',
right: 8,
top: 8,
bottom: 8,
backgroundColor: 'var(--canon-bg-surface-1',
borderRadius: 'var(--canon-radius-2)',
border: '1px solid var(--canon-border)',
zIndex: 1,
}}
/>
</>
),
];
export const Default: Story = {
args: {
name: 'Header Page',
},
};
export const WithDescription: Story = {
args: {
...Default.args,
description: 'Header Page Description',
},
};
export const WithTabs: Story = {
args: {
...Default.args,
tabs,
},
};
export const WithOptions: Story = {
args: {
...Default.args,
options,
},
};
export const WithEverything: Story = {
args: {
...Default.args,
description: 'Header Page Description',
tabs,
options,
},
};
export const WithLayout: Story = {
args: {
...WithEverything.args,
},
decorators: layoutDecorator,
parameters: {
layout: 'fullscreen',
},
};
@@ -0,0 +1,15 @@
/*
* Copyright 2024 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.
*/
@@ -13,7 +13,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { HeaderProps } from './types';
import type { HeaderPageProps } from './types';
import { Heading } from '../Heading';
import { Text } from '../Text';
import { Flex } from '../Flex';
@@ -21,19 +22,22 @@ import { Menu } from '../Menu';
import { ButtonIcon } from '../ButtonIcon';
import { RiMore2Line } from '@remixicon/react';
export const HeaderSubNav = (props: Pick<HeaderProps, 'subNavigation'>) => {
const { subNavigation } = props;
if (!subNavigation) return null;
/**
* A component that renders a header page.
*
* @public
*/
export const HeaderPage = (props: HeaderPageProps) => {
const { name, description, options } = props;
return (
<Flex pl="4" pr="2" mt="6" justify="between">
<Flex direction="column" gap="2">
<Heading variant="title4">{subNavigation.name}</Heading>
<Text>{subNavigation.description}</Text>
<Heading variant="title4">{name}</Heading>
<Text>{description}</Text>
</Flex>
<div>
{subNavigation.options && (
{options && (
<Menu.Root>
<Menu.Trigger
render={props => (
@@ -48,7 +52,7 @@ export const HeaderSubNav = (props: Pick<HeaderProps, 'subNavigation'>) => {
<Menu.Portal>
<Menu.Positioner sideOffset={4} align="end">
<Menu.Popup>
{subNavigation.options.map(option => (
{options.map(option => (
<Menu.Item
key={option.value}
onClick={() => option.onClick?.()}
@@ -0,0 +1,18 @@
/*
* Copyright 2024 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 * from './HeaderPage';
export * from './types';
@@ -0,0 +1,48 @@
/*
* Copyright 2024 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 main HeaderPage component.
*
* @public
*/
export interface HeaderPageProps {
name?: string;
description?: string;
tabs?: HeaderPageTab[];
options?: HeaderPageOption[];
}
/**
* Represents a tab item in the header page.
*
* @public
*/
export interface HeaderPageTab {
label: string;
href?: string;
}
/**
* Represents an option item in the header page.
*
* @public
*/
export interface HeaderPageOption {
label: string;
value: string;
onClick?: () => void;
}
+1
View File
@@ -28,6 +28,7 @@
@import '../components/Flex/styles.css';
@import '../components/Grid/styles.css';
@import '../components/Header/Header.styles.css';
@import '../components/HeaderPage/HeaderPage.styles.css';
@import '../components/Heading/styles.css';
@import '../components/Icon/styles.css';
@import '../components/Link/styles.css';
+2
View File
@@ -37,6 +37,8 @@ export * from './components/Button';
export * from './components/Collapsible';
export * from './components/DataTable';
export * from './components/FieldLabel';
export * from './components/Header';
export * from './components/HeaderPage';
export * from './components/Icon';
export * from './components/ButtonIcon';
export * from './components/ButtonLink';