diff --git a/.changeset/plugin-header-remove-toolbar-wrapper.md b/.changeset/plugin-header-remove-toolbar-wrapper.md new file mode 100644 index 0000000000..084eab9b61 --- /dev/null +++ b/.changeset/plugin-header-remove-toolbar-wrapper.md @@ -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. diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index 3cad0fba10..47706497d8 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -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); + } +} diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 511074f8bb..fcfacb7412 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -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'; diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.module.css b/packages/ui/src/components/PluginHeader/PluginHeader.module.css index f016ef7674..3b8aa9a698 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.module.css +++ b/packages/ui/src/components/PluginHeader/PluginHeader.module.css @@ -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); } } diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx index d0d3475dbb..faadd4fc67 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.stories.tsx @@ -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) => ( - <> -
-
- - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - - Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, - quos. - - -
- - ), - 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 => ( - <> - - } /> - } /> - } /> - - } - /> -
Custom action} - breadcrumbs={breadcrumbs} - /> - - ), -}); - -export const WithLayout = meta.story({ - decorators: layoutDecorator, - render: args => ( - <> - -
Custom action} - breadcrumbs={breadcrumbs} - /> - - ), -}); - -export const WithLayoutNoTabs = meta.story({ - decorators: layoutDecorator, - render: args => ( - <> - -
- - ), -}); - -export const WithEverything = meta.story({ - args: { - tabs, - titleLink: '/', - }, - decorators: layoutDecorator, - render: args => ( - <> - - } /> - } /> - } /> - - } - /> -
- - - - } - /> - - ), -}); - export const WithMockedURLCampaigns = meta.story({ args: { tabs, @@ -341,7 +156,7 @@ export const WithMockedURLCampaigns = meta.story({ - + Current URL is mocked to be: /campaigns @@ -363,7 +178,7 @@ export const WithMockedURLIntegrations = meta.story({ - + Current URL is mocked to be: /integrations @@ -385,7 +200,7 @@ export const WithMockedURLNoMatch = meta.story({ - + Current URL is mocked to be: /some-other-page @@ -435,7 +250,7 @@ export const WithTabsMatchingStrategies = meta.story({ - + Current URL: /mentorship/events @@ -490,7 +305,7 @@ export const WithTabsExactMatching = meta.story({ - + Current URL: /mentorship/events @@ -534,7 +349,7 @@ export const WithTabsPrefixMatchingDeep = meta.story({ - + Current URL: /catalog/users/john/details diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx index 2414e22975..9cc3161ef7 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx @@ -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(null); - const toolbarWrapperRef = useRef(null); - const toolbarContentRef = useRef(null); - const toolbarControlsRef = useRef(null); const animationFrameRef = useRef(undefined); const lastAppliedHeightRef = useRef(undefined); @@ -123,37 +123,29 @@ export const PluginHeader = (props: PluginHeaderProps) => { }; }, []); - const titleContent = ( - <> - - {title || 'Your plugin'} - - ); + const titleText = title || 'Your plugin'; - return ( -
+ const inner = ( + <>
-
-
- - {titleLink ? ( - - {titleContent} - - ) : ( -
{titleContent}
- )} -
-
-
- {actionChildren} -
+
+
{icon || }
+

+ {titleLink ? ( + + {titleText} + + ) : ( + + {titleText} + + )} +

+
{actionChildren}
{tabs && ( - + {tabs?.map(tab => ( @@ -170,6 +162,14 @@ export const PluginHeader = (props: PluginHeaderProps) => { )} + + ); + + const { bg: surfaceBg } = providerBg; + + return ( +
+ {surfaceBg ? {inner} : inner}
); }; diff --git a/packages/ui/src/components/PluginHeader/definition.ts b/packages/ui/src/components/PluginHeader/definition.ts index c9a35aa956..66414efbee 100644 --- a/packages/ui/src/components/PluginHeader/definition.ts +++ b/packages/ui/src/components/PluginHeader/definition.ts @@ -27,7 +27,6 @@ export const PluginHeaderDefinition = defineComponent()({ classNames: { root: 'bui-PluginHeader', toolbar: 'bui-PluginHeaderToolbar', - toolbarWrapper: 'bui-PluginHeaderToolbarWrapper', toolbarContent: 'bui-PluginHeaderToolbarContent', toolbarControls: 'bui-PluginHeaderToolbarControls', toolbarIcon: 'bui-PluginHeaderToolbarIcon', diff --git a/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx new file mode 100644 index 0000000000..b3fc2efc57 --- /dev/null +++ b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx @@ -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 = () => ( + + + + + + + +); + +// --------------------------------------------------------------------------- +// Shared layout decorator +// --------------------------------------------------------------------------- + +const withLayout = (Story: StoryFn) => ( + + + + + +); + +// --------------------------------------------------------------------------- +// 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: () => ( + <> + } + 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={ + <> + } + aria-label="Settings" + /> + + } + aria-label="More options" + /> + + Import component + Register existing + View documentation + + + + } + /> +
+ + + + } + /> + + + ), +}); + +// --------------------------------------------------------------------------- +// Story: CI/CD pipeline view +// --------------------------------------------------------------------------- + +export const CICDPipelineView = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } + 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={ + <> + } + aria-label="Refresh" + /> + + } + /> +
+ + + + } + /> + + + ), +}); + +// --------------------------------------------------------------------------- +// Story: TechDocs page +// --------------------------------------------------------------------------- + +export const TechDocsPage = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } + title="TechDocs" + titleLink="/" + tabs={[ + { id: 'explore', label: 'Explore', href: '/explore' }, + { id: 'owned', label: 'Owned by me', href: '/owned' }, + { id: 'starred', label: 'Starred', href: '/starred' }, + ]} + /> +
+ } + aria-label="More options" + /> + + } href="/share"> + Share link + + } href="/edit"> + Edit on GitHub + + + + } + /> + + + ), +}); + +// --------------------------------------------------------------------------- +// Story: Security / compliance audit page +// --------------------------------------------------------------------------- + +export const SecurityAuditPage = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } + 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={ + <> + } + aria-label="Refresh scan" + /> + + + } + /> +
+ + + } + /> + + + ), +}); + +// --------------------------------------------------------------------------- +// Story: Minimal — no tabs, no actions +// --------------------------------------------------------------------------- + +export const Minimal = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } title="APIs" titleLink="/" /> +
+ + + ), +});