First pass improving the PluginHeader
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed the `toolbarWrapper` element from `PluginHeader` and dropped `toolbarWrapper` from `PluginHeaderDefinition.classNames`. Toolbar layout styles now live on `toolbar` (`.bui-PluginHeaderToolbar`). Update custom CSS that targeted `.bui-PluginHeaderToolbarWrapper` to use `.bui-PluginHeaderToolbar` instead.
|
||||
|
||||
**Affected components:** PluginHeader
|
||||
|
||||
`PluginHeader` now establishes a neutral background provider (same rules as `Box` with `bg="neutral"`) so controls in the toolbar and tabs resolve `data-on-bg` correctly.
|
||||
@@ -183,21 +183,6 @@
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
}
|
||||
|
||||
.bui-PluginHeaderToolbarWrapper {
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.bui-PluginHeaderTabsWrapper {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: -8px;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.bui-Input {
|
||||
border-radius: var(--bui-radius-3);
|
||||
}
|
||||
@@ -228,14 +213,6 @@
|
||||
--bui-border-success: #53db83;
|
||||
|
||||
--bui-ring: rgba(0, 0, 0, 0.2);
|
||||
|
||||
.bui-HeaderToolbarWrapper {
|
||||
border: 1px solid var(--bui-border-2);
|
||||
}
|
||||
|
||||
.bui-HeaderTabsWrapper {
|
||||
border: 1px solid var(--bui-border-2);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme-mode='dark'][data-theme-name='spotify'] {
|
||||
@@ -266,3 +243,34 @@
|
||||
|
||||
--bui-ring: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Plugin header (@backstage/ui) and story shell header — kept at the bottom of
|
||||
* this file for easier scanning alongside other component overrides above.
|
||||
*/
|
||||
[data-theme-name='spotify'] {
|
||||
.bui-PluginHeaderToolbar {
|
||||
padding: 0;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: none;
|
||||
margin-bottom: var(--bui-space-2);
|
||||
}
|
||||
|
||||
.bui-PluginHeaderTabsWrapper {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme-mode='light'][data-theme-name='spotify'] {
|
||||
.bui-HeaderToolbarWrapper {
|
||||
border: 1px solid var(--bui-border-2);
|
||||
}
|
||||
|
||||
.bui-HeaderTabsWrapper {
|
||||
border: 1px solid var(--bui-border-2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1949,7 +1949,6 @@ export const PluginHeaderDefinition: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-PluginHeader';
|
||||
readonly toolbar: 'bui-PluginHeaderToolbar';
|
||||
readonly toolbarWrapper: 'bui-PluginHeaderToolbarWrapper';
|
||||
readonly toolbarContent: 'bui-PluginHeaderToolbarContent';
|
||||
readonly toolbarControls: 'bui-PluginHeaderToolbarControls';
|
||||
readonly toolbarIcon: 'bui-PluginHeaderToolbarIcon';
|
||||
|
||||
@@ -17,15 +17,16 @@
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.bui-PluginHeaderToolbarWrapper {
|
||||
.bui-PluginHeaderToolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-inline: var(--bui-space-5);
|
||||
border-bottom: 1px solid var(--bui-border-1);
|
||||
color: var(--bui-fg-primary);
|
||||
height: 52px;
|
||||
background-color: var(--bui-bg-neutral-1);
|
||||
border-bottom: 1px solid var(--bui-border-1);
|
||||
}
|
||||
|
||||
.bui-PluginHeaderToolbarContent {
|
||||
@@ -46,13 +47,19 @@
|
||||
}
|
||||
|
||||
.bui-PluginHeaderToolbarIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: var(--bui-radius-2);
|
||||
background-color: var(--bui-bg-neutral-2);
|
||||
color: var(--bui-fg-primary);
|
||||
|
||||
& svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,5 +73,6 @@
|
||||
.bui-PluginHeaderTabsWrapper {
|
||||
padding-inline: var(--bui-space-3);
|
||||
border-bottom: 1px solid var(--bui-border-1);
|
||||
background-color: var(--bui-bg-neutral-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ import type { StoryFn } from '@storybook/react-vite';
|
||||
import { PluginHeader } from './PluginHeader';
|
||||
import type { HeaderTab } from './types';
|
||||
import {
|
||||
Button,
|
||||
Header,
|
||||
Container,
|
||||
Text,
|
||||
ButtonIcon,
|
||||
@@ -36,7 +34,6 @@ import {
|
||||
RiCloudy2Line,
|
||||
RiMore2Line,
|
||||
} from '@remixicon/react';
|
||||
import { HeaderBreadcrumb } from '../Header/types';
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Backstage UI/PluginHeader',
|
||||
@@ -82,24 +79,6 @@ const tabs: HeaderTab[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const tabs2: HeaderTab[] = [
|
||||
{
|
||||
id: 'Banana',
|
||||
label: 'Banana',
|
||||
href: '/banana',
|
||||
},
|
||||
{
|
||||
id: 'Apple',
|
||||
label: 'Apple',
|
||||
href: '/apple',
|
||||
},
|
||||
{
|
||||
id: 'Orange',
|
||||
label: 'Orange',
|
||||
href: '/orange',
|
||||
},
|
||||
];
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: 'Settings',
|
||||
@@ -120,86 +99,6 @@ const menuItems = [
|
||||
},
|
||||
];
|
||||
|
||||
const breadcrumbs: HeaderBreadcrumb[] = [
|
||||
{
|
||||
label: 'Home',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
label: 'Dashboard',
|
||||
href: '/dashboard',
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
href: '/settings',
|
||||
},
|
||||
];
|
||||
|
||||
// Extract layout decorator as a reusable constant
|
||||
const layoutDecorator = [
|
||||
(Story: StoryFn) => (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
width: '250px',
|
||||
position: 'fixed',
|
||||
left: 'var(--sb-panel-left)',
|
||||
top: 'var(--sb-panel-top)',
|
||||
bottom: 'var(--sb-panel-bottom)',
|
||||
backgroundColor: 'var(--sb-sidebar-bg)',
|
||||
borderRadius: 'var(--sb-panel-radius)',
|
||||
border: 'var(--sb-sidebar-border)',
|
||||
borderRight: 'var(--sb-sidebar-border-right)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 'var(--sb-content-padding-inline)',
|
||||
minHeight: '200vh',
|
||||
}}
|
||||
>
|
||||
<Story />
|
||||
<Container>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
<Text as="p">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam,
|
||||
quos.
|
||||
</Text>
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
withRouter,
|
||||
];
|
||||
|
||||
export const Default = meta.story({
|
||||
args: {},
|
||||
decorators: [withRouter],
|
||||
@@ -249,90 +148,6 @@ export const WithAllOptionsAndTabs = WithCustomActions.extend({
|
||||
},
|
||||
});
|
||||
|
||||
export const WithHeader = meta.story({
|
||||
args: {
|
||||
...WithAllOptionsAndTabs.input.args,
|
||||
},
|
||||
decorators: [withRouter],
|
||||
render: args => (
|
||||
<>
|
||||
<PluginHeader
|
||||
{...args}
|
||||
customActions={
|
||||
<>
|
||||
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
|
||||
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
|
||||
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Header
|
||||
title="Page title"
|
||||
tabs={tabs2}
|
||||
customActions={<Button>Custom action</Button>}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithLayout = meta.story({
|
||||
decorators: layoutDecorator,
|
||||
render: args => (
|
||||
<>
|
||||
<PluginHeader {...args} tabs={tabs} />
|
||||
<Header
|
||||
title="Page title"
|
||||
tabs={tabs2}
|
||||
customActions={<Button>Custom action</Button>}
|
||||
breadcrumbs={breadcrumbs}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithLayoutNoTabs = meta.story({
|
||||
decorators: layoutDecorator,
|
||||
render: args => (
|
||||
<>
|
||||
<PluginHeader {...args} />
|
||||
<Header title="Page title" tabs={tabs2} />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithEverything = meta.story({
|
||||
args: {
|
||||
tabs,
|
||||
titleLink: '/',
|
||||
},
|
||||
decorators: layoutDecorator,
|
||||
render: args => (
|
||||
<>
|
||||
<PluginHeader
|
||||
{...args}
|
||||
customActions={
|
||||
<>
|
||||
<ButtonIcon variant="tertiary" icon={<RiCloudy2Line />} />
|
||||
<ButtonIcon variant="tertiary" icon={<RiEmotionHappyLine />} />
|
||||
<ButtonIcon variant="tertiary" icon={<RiHeartLine />} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Header
|
||||
title="Page title"
|
||||
tabs={tabs2}
|
||||
customActions={
|
||||
<>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="primary">Primary</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
export const WithMockedURLCampaigns = meta.story({
|
||||
args: {
|
||||
tabs,
|
||||
@@ -341,7 +156,7 @@ export const WithMockedURLCampaigns = meta.story({
|
||||
<MemoryRouter initialEntries={['/campaigns']}>
|
||||
<BUIProvider>
|
||||
<PluginHeader {...args} />
|
||||
<Container>
|
||||
<Container mt="6">
|
||||
<Text as="p">
|
||||
Current URL is mocked to be: <strong>/campaigns</strong>
|
||||
</Text>
|
||||
@@ -363,7 +178,7 @@ export const WithMockedURLIntegrations = meta.story({
|
||||
<MemoryRouter initialEntries={['/integrations']}>
|
||||
<BUIProvider>
|
||||
<PluginHeader {...args} />
|
||||
<Container>
|
||||
<Container mt="6">
|
||||
<Text as="p">
|
||||
Current URL is mocked to be: <strong>/integrations</strong>
|
||||
</Text>
|
||||
@@ -385,7 +200,7 @@ export const WithMockedURLNoMatch = meta.story({
|
||||
<MemoryRouter initialEntries={['/some-other-page']}>
|
||||
<BUIProvider>
|
||||
<PluginHeader {...args} />
|
||||
<Container>
|
||||
<Container mt="6">
|
||||
<Text as="p">
|
||||
Current URL is mocked to be: <strong>/some-other-page</strong>
|
||||
</Text>
|
||||
@@ -435,7 +250,7 @@ export const WithTabsMatchingStrategies = meta.story({
|
||||
<MemoryRouter initialEntries={['/mentorship/events']}>
|
||||
<BUIProvider>
|
||||
<PluginHeader {...args} />
|
||||
<Container>
|
||||
<Container mt="6">
|
||||
<Text>
|
||||
<strong>Current URL:</strong> /mentorship/events
|
||||
</Text>
|
||||
@@ -490,7 +305,7 @@ export const WithTabsExactMatching = meta.story({
|
||||
<MemoryRouter initialEntries={['/mentorship/events']}>
|
||||
<BUIProvider>
|
||||
<PluginHeader {...args} />
|
||||
<Container>
|
||||
<Container mt="6">
|
||||
<Text>
|
||||
<strong>Current URL:</strong> /mentorship/events
|
||||
</Text>
|
||||
@@ -534,7 +349,7 @@ export const WithTabsPrefixMatchingDeep = meta.story({
|
||||
<MemoryRouter initialEntries={['/catalog/users/john/details']}>
|
||||
<BUIProvider>
|
||||
<PluginHeader {...args} />
|
||||
<Container>
|
||||
<Container mt="6">
|
||||
<Text as="p">
|
||||
<strong>Current URL:</strong> /catalog/users/john/details
|
||||
</Text>
|
||||
|
||||
@@ -17,12 +17,13 @@
|
||||
import type { PluginHeaderProps } from './types';
|
||||
import { Tabs, TabList, Tab } from '../Tabs';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { useBgProvider, BgProvider } from '../../hooks/useBg';
|
||||
import { PluginHeaderDefinition } from './definition';
|
||||
import { type NavigateOptions } from 'react-router-dom';
|
||||
import { Children, useMemo, useRef } from 'react';
|
||||
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
|
||||
import { Box } from '../Box';
|
||||
import { Link } from 'react-aria-components';
|
||||
import { Link } from '../Link';
|
||||
import { RiShapesLine } from '@remixicon/react';
|
||||
import { Text } from '../Text';
|
||||
|
||||
@@ -33,8 +34,9 @@ declare module 'react-aria-components' {
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that renders a plugin header with icon, title, custom actions,
|
||||
* and navigation tabs.
|
||||
* Renders a plugin header with icon, title, custom actions, and optional tabs.
|
||||
* Always participates in the background context system so descendants (e.g. buttons)
|
||||
* get the correct `data-on-bg` styling inside the toolbar and tabs.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -50,11 +52,9 @@ export const PluginHeader = (props: PluginHeaderProps) => {
|
||||
onTabSelectionChange,
|
||||
} = ownProps;
|
||||
|
||||
const providerBg = useBgProvider('neutral');
|
||||
const hasTabs = tabs && tabs.length > 0;
|
||||
const headerRef = useRef<HTMLElement>(null);
|
||||
const toolbarWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarContentRef = useRef<HTMLDivElement>(null);
|
||||
const toolbarControlsRef = useRef<HTMLDivElement>(null);
|
||||
const animationFrameRef = useRef<number | undefined>(undefined);
|
||||
const lastAppliedHeightRef = useRef<number | undefined>(undefined);
|
||||
|
||||
@@ -123,37 +123,29 @@ export const PluginHeader = (props: PluginHeaderProps) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const titleContent = (
|
||||
<>
|
||||
<div className={classes.toolbarIcon} aria-hidden="true">
|
||||
{icon || <RiShapesLine />}
|
||||
</div>
|
||||
<Text variant="body-medium">{title || 'Your plugin'}</Text>
|
||||
</>
|
||||
);
|
||||
const titleText = title || 'Your plugin';
|
||||
|
||||
return (
|
||||
<header ref={headerRef} className={classes.root}>
|
||||
const inner = (
|
||||
<>
|
||||
<div className={classes.toolbar} data-has-tabs={hasTabs}>
|
||||
<div className={classes.toolbarWrapper} ref={toolbarWrapperRef}>
|
||||
<div className={classes.toolbarContent} ref={toolbarContentRef}>
|
||||
<Text as="h1" variant="body-medium">
|
||||
{titleLink ? (
|
||||
<Link className={classes.toolbarName} href={titleLink}>
|
||||
{titleContent}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={classes.toolbarName}>{titleContent}</div>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={classes.toolbarControls} ref={toolbarControlsRef}>
|
||||
{actionChildren}
|
||||
</div>
|
||||
<div className={classes.toolbarContent}>
|
||||
<div className={classes.toolbarIcon}>{icon || <RiShapesLine />}</div>
|
||||
<h1 className={classes.toolbarName}>
|
||||
{titleLink ? (
|
||||
<Link href={titleLink} standalone variant="body-medium">
|
||||
{titleText}
|
||||
</Link>
|
||||
) : (
|
||||
<Text as="span" variant="body-medium">
|
||||
{titleText}
|
||||
</Text>
|
||||
)}
|
||||
</h1>
|
||||
</div>
|
||||
<div className={classes.toolbarControls}>{actionChildren}</div>
|
||||
</div>
|
||||
{tabs && (
|
||||
<Box bg="neutral" className={classes.tabs}>
|
||||
<Box className={classes.tabs}>
|
||||
<Tabs onSelectionChange={onTabSelectionChange}>
|
||||
<TabList>
|
||||
{tabs?.map(tab => (
|
||||
@@ -170,6 +162,14 @@ export const PluginHeader = (props: PluginHeaderProps) => {
|
||||
</Tabs>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const { bg: surfaceBg } = providerBg;
|
||||
|
||||
return (
|
||||
<header ref={headerRef} className={classes.root} data-bg={surfaceBg}>
|
||||
{surfaceBg ? <BgProvider bg={surfaceBg}>{inner}</BgProvider> : inner}
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,7 +27,6 @@ export const PluginHeaderDefinition = defineComponent<PluginHeaderOwnProps>()({
|
||||
classNames: {
|
||||
root: 'bui-PluginHeader',
|
||||
toolbar: 'bui-PluginHeaderToolbar',
|
||||
toolbarWrapper: 'bui-PluginHeaderToolbarWrapper',
|
||||
toolbarContent: 'bui-PluginHeaderToolbarContent',
|
||||
toolbarControls: 'bui-PluginHeaderToolbarControls',
|
||||
toolbarIcon: 'bui-PluginHeaderToolbarIcon',
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* 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 { MemoryRouter } from 'react-router-dom';
|
||||
import { BUIProvider } from '../provider';
|
||||
import {
|
||||
PluginHeader,
|
||||
Header,
|
||||
Button,
|
||||
ButtonIcon,
|
||||
Card,
|
||||
Container,
|
||||
Flex,
|
||||
MenuTrigger,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '..';
|
||||
import {
|
||||
RiBookOpenLine,
|
||||
RiBox3Line,
|
||||
RiCodeSSlashLine,
|
||||
RiDownloadLine,
|
||||
RiEdit2Line,
|
||||
RiGitBranchLine,
|
||||
RiMore2Line,
|
||||
RiPlayLine,
|
||||
RiRefreshLine,
|
||||
RiSettings4Line,
|
||||
RiShieldCheckLine,
|
||||
RiShareBoxLine,
|
||||
RiTerminalLine,
|
||||
} from '@remixicon/react';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared page content placeholder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PageContent = () => (
|
||||
<Container mt="6">
|
||||
<Flex direction="row" gap="4">
|
||||
<Card style={{ minHeight: 120, flex: 1 }} />
|
||||
<Card style={{ minHeight: 120, flex: 1 }} />
|
||||
<Card style={{ minHeight: 120, flex: 1 }} />
|
||||
</Flex>
|
||||
</Container>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared layout decorator
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const withLayout = (Story: StoryFn) => (
|
||||
<MemoryRouter>
|
||||
<BUIProvider>
|
||||
<Story />
|
||||
</BUIProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Meta
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const meta = preview.meta({
|
||||
title: 'Recipes/PluginHeader and Header',
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Story: Catalog entity page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const CatalogEntityPage = meta.story({
|
||||
decorators: [withLayout],
|
||||
render: () => (
|
||||
<>
|
||||
<PluginHeader
|
||||
icon={<RiBox3Line />}
|
||||
title="Catalog"
|
||||
titleLink="/"
|
||||
tabs={[
|
||||
{ id: 'catalog', label: 'Catalog', href: '/catalog' },
|
||||
{ id: 'apis', label: 'APIs', href: '/apis' },
|
||||
{ id: 'resources', label: 'Resources', href: '/resources' },
|
||||
{ id: 'templates', label: 'Templates', href: '/templates' },
|
||||
{ id: 'docs', label: 'Docs', href: '/docs' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
<ButtonIcon
|
||||
variant="secondary"
|
||||
icon={<RiSettings4Line />}
|
||||
aria-label="Settings"
|
||||
/>
|
||||
<MenuTrigger>
|
||||
<ButtonIcon
|
||||
variant="secondary"
|
||||
icon={<RiMore2Line />}
|
||||
aria-label="More options"
|
||||
/>
|
||||
<Menu placement="bottom end">
|
||||
<MenuItem href="/catalog/import">Import component</MenuItem>
|
||||
<MenuItem href="/catalog/register">Register existing</MenuItem>
|
||||
<MenuItem href="/catalog/docs">View documentation</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Header
|
||||
title="payment-service"
|
||||
breadcrumbs={[
|
||||
{ label: 'Catalog', href: '/catalog' },
|
||||
{ label: 'Services', href: '/catalog?kind=Component' },
|
||||
]}
|
||||
tabs={[
|
||||
{ id: 'overview', label: 'Overview', href: '/overview' },
|
||||
{ id: 'ci-cd', label: 'CI/CD', href: '/ci-cd' },
|
||||
{ id: 'api', label: 'API', href: '/api' },
|
||||
{ id: 'dependencies', label: 'Dependencies', href: '/dependencies' },
|
||||
{ id: 'docs', label: 'Docs', href: '/docs' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
<Button variant="secondary" iconStart={<RiEdit2Line />}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button variant="primary" iconStart={<RiShareBoxLine />}>
|
||||
Unregister
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<PageContent />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Story: CI/CD pipeline view
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const CICDPipelineView = meta.story({
|
||||
decorators: [withLayout],
|
||||
render: () => (
|
||||
<>
|
||||
<PluginHeader
|
||||
icon={<RiGitBranchLine />}
|
||||
title="CI/CD"
|
||||
titleLink="/"
|
||||
tabs={[
|
||||
{ id: 'builds', label: 'Builds', href: '/builds' },
|
||||
{ id: 'pipelines', label: 'Pipelines', href: '/pipelines' },
|
||||
{ id: 'deployments', label: 'Deployments', href: '/deployments' },
|
||||
{ id: 'settings', label: 'Settings', href: '/settings' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
<ButtonIcon
|
||||
variant="tertiary"
|
||||
icon={<RiRefreshLine />}
|
||||
aria-label="Refresh"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Header
|
||||
title="main · #842"
|
||||
tabs={[
|
||||
{ id: 'summary', label: 'Summary', href: '/summary' },
|
||||
{ id: 'steps', label: 'Steps', href: '/steps' },
|
||||
{ id: 'artifacts', label: 'Artifacts', href: '/artifacts' },
|
||||
{ id: 'logs', label: 'Logs', href: '/logs' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
<Button variant="secondary" iconStart={<RiDownloadLine />}>
|
||||
Download logs
|
||||
</Button>
|
||||
<Button variant="primary" iconStart={<RiPlayLine />}>
|
||||
Re-run pipeline
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<PageContent />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Story: TechDocs page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const TechDocsPage = meta.story({
|
||||
decorators: [withLayout],
|
||||
render: () => (
|
||||
<>
|
||||
<PluginHeader
|
||||
icon={<RiBookOpenLine />}
|
||||
title="TechDocs"
|
||||
titleLink="/"
|
||||
tabs={[
|
||||
{ id: 'explore', label: 'Explore', href: '/explore' },
|
||||
{ id: 'owned', label: 'Owned by me', href: '/owned' },
|
||||
{ id: 'starred', label: 'Starred', href: '/starred' },
|
||||
]}
|
||||
/>
|
||||
<Header
|
||||
title="Getting started"
|
||||
tabs={[
|
||||
{ id: 'overview', label: 'Overview', href: '/overview' },
|
||||
{
|
||||
id: 'architecture',
|
||||
label: 'Architecture',
|
||||
href: '/architecture',
|
||||
},
|
||||
{ id: 'runbooks', label: 'Runbooks', href: '/runbooks' },
|
||||
{ id: 'adr', label: 'ADRs', href: '/adr' },
|
||||
]}
|
||||
customActions={
|
||||
<MenuTrigger>
|
||||
<ButtonIcon
|
||||
variant="tertiary"
|
||||
icon={<RiMore2Line />}
|
||||
aria-label="More options"
|
||||
/>
|
||||
<Menu placement="bottom end">
|
||||
<MenuItem iconStart={<RiShareBoxLine />} href="/share">
|
||||
Share link
|
||||
</MenuItem>
|
||||
<MenuItem iconStart={<RiEdit2Line />} href="/edit">
|
||||
Edit on GitHub
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</MenuTrigger>
|
||||
}
|
||||
/>
|
||||
<PageContent />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Story: Security / compliance audit page
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const SecurityAuditPage = meta.story({
|
||||
decorators: [withLayout],
|
||||
render: () => (
|
||||
<>
|
||||
<PluginHeader
|
||||
icon={<RiShieldCheckLine />}
|
||||
title="Security"
|
||||
titleLink="/"
|
||||
tabs={[
|
||||
{ id: 'overview', label: 'Overview', href: '/overview' },
|
||||
{ id: 'vulnerabilities', label: 'Vulnerabilities', href: '/vulns' },
|
||||
{ id: 'policies', label: 'Policies', href: '/policies' },
|
||||
{ id: 'audits', label: 'Audits', href: '/audits' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
<ButtonIcon
|
||||
variant="tertiary"
|
||||
icon={<RiRefreshLine />}
|
||||
aria-label="Refresh scan"
|
||||
/>
|
||||
<Button variant="primary" iconStart={<RiTerminalLine />}>
|
||||
Run scan
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Header
|
||||
title="payment-service"
|
||||
tabs={[
|
||||
{ id: 'critical', label: 'Critical', href: '/critical' },
|
||||
{ id: 'high', label: 'High', href: '/high' },
|
||||
{ id: 'medium', label: 'Medium', href: '/medium' },
|
||||
{ id: 'low', label: 'Low', href: '/low' },
|
||||
]}
|
||||
customActions={
|
||||
<>
|
||||
<Button variant="secondary" iconStart={<RiDownloadLine />}>
|
||||
Export report
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<PageContent />
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Story: Minimal — no tabs, no actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const Minimal = meta.story({
|
||||
decorators: [withLayout],
|
||||
render: () => (
|
||||
<>
|
||||
<PluginHeader icon={<RiCodeSSlashLine />} title="APIs" titleLink="/" />
|
||||
<Header title="payments-api" />
|
||||
<PageContent />
|
||||
</>
|
||||
),
|
||||
});
|
||||
Reference in New Issue
Block a user