diff --git a/.changeset/core-header-marker.md b/.changeset/core-header-marker.md new file mode 100644 index 0000000000..a523c6f0b7 --- /dev/null +++ b/.changeset/core-header-marker.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added stable DOM markers to the legacy Page and Header so adjacent layout components can coordinate spacing without relying on generated class names. diff --git a/.changeset/plugin-header-spacing.md b/.changeset/plugin-header-spacing.md new file mode 100644 index 0000000000..a1c370ea19 --- /dev/null +++ b/.changeset/plugin-header-spacing.md @@ -0,0 +1,7 @@ +--- +'@backstage/ui': patch +--- + +Adjusted PluginHeader spacing and borders so headers with and without tabs align more consistently with surrounding page content, including when paired with page headers. + +**Affected components:** PluginHeader, Header diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index f91eb62e02..7a34299817 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -239,27 +239,3 @@ --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 { - height: 32px; - border: none; - background: none; - margin-top: var(--bui-space-4); - } - - .bui-PluginHeaderTabsWrapper { - border: none; - background: none; - margin-left: 0; - margin-top: var(--bui-space-2); - } - - .bui-HeaderTop { - padding-top: var(--bui-space-4); - } -} diff --git a/packages/core-components/src/layout/Header/Header.tsx b/packages/core-components/src/layout/Header/Header.tsx index f5facda641..3320ba782a 100644 --- a/packages/core-components/src/layout/Header/Header.tsx +++ b/packages/core-components/src/layout/Header/Header.tsx @@ -228,7 +228,11 @@ export function Header(props: PropsWithChildren) { return ( <> -
+
-
{children}
+
+ {children} +
); } diff --git a/packages/ui/package.json b/packages/ui/package.json index a8c28464ed..c68a85b7e5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -60,6 +60,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@backstage/core-components": "workspace:^", "@storybook/react-vite": "^10.3.3", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^16.0.0", diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index 58bcf2699d..60db85cb25 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -32,10 +32,6 @@ box-sizing: border-box; } - .bui-HeaderTop { - padding-top: var(--bui-space-6); - } - .bui-HeaderStickySentinel { height: 1px; margin-bottom: -1px; diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.module.css b/packages/ui/src/components/PluginHeader/PluginHeader.module.css index 1ab0f9570a..31c827a06b 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.module.css +++ b/packages/ui/src/components/PluginHeader/PluginHeader.module.css @@ -17,15 +17,57 @@ @layer tokens, base, components, utilities; @layer components { + .bui-PluginHeader { + --bui-plugin-header-margin-bottom: var(--bui-space-6); + --bui-plugin-header-toolbar-border-bottom: solid 1px var(--bui-border-1); + --bui-plugin-header-tabs-border-bottom: 1px solid var(--bui-border-1); + --bui-plugin-header-toolbar-padding-bottom: var(--bui-space-4); + --bui-plugin-header-toolbar-tabs-padding-bottom: var(--bui-space-1); + --bui-plugin-header-background-color: transparent; + --bui-plugin-header-padding-top: var(--bui-space-4); + + margin-bottom: var(--bui-plugin-header-margin-bottom); + padding-inline: var(--bui-space-5); + padding-top: var(--bui-plugin-header-padding-top); + background-color: var(--bui-plugin-header-background-color); + } + + .bui-PluginHeader:has(+ .bui-HeaderTop), + .bui-PluginHeader:has(+ [data-backstage-core-header]), + .bui-PluginHeader:has( + + [data-backstage-core-page] [data-backstage-core-header] + ) { + --bui-plugin-header-margin-bottom: 0; + --bui-plugin-header-toolbar-border-bottom: none; + --bui-plugin-header-tabs-border-bottom: none; + --bui-plugin-header-toolbar-padding-bottom: var(--bui-space-2); + --bui-plugin-header-toolbar-tabs-padding-bottom: var(--bui-space-2); + --bui-plugin-header-background-color: var(--bui-bg-neutral-1); + --bui-plugin-header-padding-top: var(--bui-space-2); + } + + .bui-PluginHeader:has(+ [data-backstage-core-header]) + .bui-PluginHeaderToolbarIcon, + .bui-PluginHeader:has( + + [data-backstage-core-page] [data-backstage-core-header] + ) + .bui-PluginHeaderToolbarIcon { + background-color: var(--bui-bg-neutral-3); + } + .bui-PluginHeaderToolbar { display: flex; flex-direction: row; align-items: center; justify-content: space-between; - padding-inline: var(--bui-space-5); color: var(--bui-fg-primary); - height: 52px; - border-bottom: 1px solid var(--bui-border-1); + border-bottom: var(--bui-plugin-header-toolbar-border-bottom); + padding-bottom: var(--bui-plugin-header-toolbar-padding-bottom); + + &[data-has-tabs] { + border-bottom: none; + padding-bottom: var(--bui-plugin-header-toolbar-tabs-padding-bottom); + } } .bui-PluginHeaderToolbarContent { @@ -45,7 +87,7 @@ flex-shrink: 0; } - .bui-PluginHeaderToolbarIcon { + .bui-PluginHeader .bui-PluginHeaderToolbarIcon { display: flex; align-items: center; justify-content: center; @@ -69,7 +111,6 @@ } .bui-PluginHeaderTabsWrapper { - padding-inline: var(--bui-space-3); - border-bottom: 1px solid var(--bui-border-1); + border-bottom: var(--bui-plugin-header-tabs-border-bottom); } } diff --git a/packages/ui/src/components/PluginHeader/PluginHeader.tsx b/packages/ui/src/components/PluginHeader/PluginHeader.tsx index e410ce91f9..6e94bbe159 100644 --- a/packages/ui/src/components/PluginHeader/PluginHeader.tsx +++ b/packages/ui/src/components/PluginHeader/PluginHeader.tsx @@ -25,7 +25,6 @@ import { Box } from '../Box'; import { Link } from '../Link'; import { RiShapesLine } from '@remixicon/react'; import { Text } from '../Text'; -import { BgReset } from '../../hooks/useBg'; declare module 'react-aria-components' { interface RouterConfig { @@ -53,7 +52,7 @@ export const PluginHeader = (props: PluginHeaderProps) => { } = ownProps; const hasTabs = tabs && tabs.length > 0; - const headerRef = useRef(null); + const rootRef = useRef(null); const animationFrameRef = useRef(undefined); const lastAppliedHeightRef = useRef(undefined); @@ -62,7 +61,7 @@ export const PluginHeader = (props: PluginHeaderProps) => { }, [customActions]); useIsomorphicLayoutEffect(() => { - const el = headerRef.current; + const el = rootRef.current; if (!el) { return undefined; } @@ -125,50 +124,44 @@ export const PluginHeader = (props: PluginHeaderProps) => { const titleText = title || 'Your plugin'; return ( - -
- -
- -

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

-
-
{actionChildren}
-
- {tabs && ( - - - - {tabs?.map(tab => ( - - {tab.label} - - ))} - - +
+
+
+ - )} -
-
+

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

+ +
{actionChildren}
+ + {hasTabs && ( +
+ + + {tabs?.map(tab => ( + + {tab.label} + + ))} + + +
+ )} + ); }; diff --git a/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx index 4f74572ef5..66e659e28c 100644 --- a/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx +++ b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx @@ -15,6 +15,10 @@ */ import preview from '../../../../.storybook/preview'; +import { + Header as CoreHeader, + Page as CorePage, +} from '@backstage/core-components'; import type { StoryFn } from '@storybook/react-vite'; import { MemoryRouter } from 'react-router-dom'; import { BUIProvider } from '../provider'; @@ -49,7 +53,7 @@ import { // --------------------------------------------------------------------------- const PageContent = () => ( - + @@ -59,7 +63,7 @@ const PageContent = () => ( ); const LongPageContent = () => ( - + @@ -104,6 +108,28 @@ export const NoHeader = meta.story({ ), }); +export const NoHeaderWithTabs = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } + title="APIs" + tabs={[ + { id: 'overview', label: 'Overview', href: '/apis' }, + { + id: 'definitions', + label: 'Definitions', + href: '/apis/definitions', + }, + { id: 'consumers', label: 'Consumers', href: '/apis/consumers' }, + ]} + /> + + + ), +}); + export const SimpleHeader = meta.story({ decorators: [withLayout], render: () => ( @@ -115,6 +141,44 @@ export const SimpleHeader = meta.story({ ), }); +export const CoreComponentsHeader = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } title="APIs" /> + + + + + + ), +}); + +export const CoreComponentsHeaderWithTabs = meta.story({ + decorators: [withLayout], + render: () => ( + <> + } + title="APIs" + tabs={[ + { id: 'overview', label: 'Overview', href: '/apis' }, + { + id: 'definitions', + label: 'Definitions', + href: '/apis/definitions', + }, + { id: 'consumers', label: 'Consumers', href: '/apis/consumers' }, + ]} + /> + + + + + + ), +}); + export const WithTabs = meta.story({ decorators: [withLayout], render: () => ( diff --git a/yarn.lock b/yarn.lock index 37c405d2f9..b11895427f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7949,6 +7949,7 @@ __metadata: resolution: "@backstage/ui@workspace:packages/ui" dependencies: "@backstage/cli": "workspace:^" + "@backstage/core-components": "workspace:^" "@backstage/version-bridge": "workspace:^" "@braintree/sanitize-url": "npm:^7.1.2" "@internationalized/date": "npm:^3.12.0"