From 8a0a5c955306c0316b56bd6d8cde35e59de9a808 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Sun, 26 Apr 2026 20:36:24 +0100 Subject: [PATCH] feat(ui): improve Header layout with tag-aware padding and renamed sections - Add data-has-tags attribute to reduce header padding-top when tags are present - Rename beforeSticky/afterSticky classes to headerTop/headerBottom - Always render headerBottom for consistent bottom margin - Inline JSX sections into a single return statement Signed-off-by: Charles de Dreuille --- .storybook/themes/spotify.css | 7 +- packages/ui/report.api.md | 8 +- .../src/components/Header/Header.module.css | 17 +- packages/ui/src/components/Header/Header.tsx | 242 +++++++++--------- .../ui/src/components/Header/definition.ts | 4 +- .../recipes/PluginHeaderAndHeader.stories.tsx | 108 ++++---- 6 files changed, 193 insertions(+), 193 deletions(-) diff --git a/.storybook/themes/spotify.css b/.storybook/themes/spotify.css index d8f06c68c0..6478bf1112 100644 --- a/.storybook/themes/spotify.css +++ b/.storybook/themes/spotify.css @@ -246,17 +246,16 @@ */ [data-theme-name='spotify'] { .bui-PluginHeaderToolbar { - padding: 0; height: 32px; border: none; background: none; - margin-bottom: var(--bui-space-2); + margin-top: var(--bui-space-4); } .bui-PluginHeaderTabsWrapper { - padding: 0; border: none; background: none; - margin-left: -8px; + margin-left: 0; + margin-top: var(--bui-space-2); } } diff --git a/packages/ui/report.api.md b/packages/ui/report.api.md index 85f59f178f..7b4cc74ee1 100644 --- a/packages/ui/report.api.md +++ b/packages/ui/report.api.md @@ -1582,10 +1582,10 @@ export const HeaderDefinition: { readonly bg: 'consumer'; readonly classNames: { readonly root: 'bui-Header'; - readonly beforeSticky: 'bui-HeaderBeforeSticky'; + readonly headerTop: 'bui-HeaderTop'; readonly stickySentinel: 'bui-HeaderStickySentinel'; readonly content: 'bui-HeaderContent'; - readonly afterSticky: 'bui-HeaderAfterSticky'; + readonly headerBottom: 'bui-HeaderBottom'; readonly breadcrumbs: 'bui-HeaderBreadcrumbs'; readonly breadcrumbsSmall: 'bui-HeaderBreadcrumbsSmall'; readonly breadcrumbLink: 'bui-HeaderBreadcrumbLink'; @@ -1766,10 +1766,10 @@ export const HeaderPageDefinition: { readonly bg: 'consumer'; readonly classNames: { readonly root: 'bui-Header'; - readonly beforeSticky: 'bui-HeaderBeforeSticky'; + readonly headerTop: 'bui-HeaderTop'; readonly stickySentinel: 'bui-HeaderStickySentinel'; readonly content: 'bui-HeaderContent'; - readonly afterSticky: 'bui-HeaderAfterSticky'; + readonly headerBottom: 'bui-HeaderBottom'; readonly breadcrumbs: 'bui-HeaderBreadcrumbs'; readonly breadcrumbsSmall: 'bui-HeaderBreadcrumbsSmall'; readonly breadcrumbLink: 'bui-HeaderBreadcrumbLink'; diff --git a/packages/ui/src/components/Header/Header.module.css b/packages/ui/src/components/Header/Header.module.css index a0387952e3..c248a5e925 100644 --- a/packages/ui/src/components/Header/Header.module.css +++ b/packages/ui/src/components/Header/Header.module.css @@ -17,25 +17,26 @@ @layer tokens, base, components, utilities; @layer components { - .bui-HeaderAfterSticky { + .bui-HeaderBottom { display: flex; flex-direction: column; gap: var(--bui-space-3); + margin-bottom: var(--bui-space-5); } - .bui-HeaderBeforeSticky, + .bui-HeaderTop, .bui-HeaderContent, - .bui-HeaderAfterSticky { + .bui-HeaderBottom { width: 100%; padding-inline: var(--bui-space-5); box-sizing: border-box; } - .bui-HeaderBeforeSticky { + .bui-HeaderTop { padding-top: var(--bui-space-4); } - .bui-HeaderBeforeSticky[data-sticky] { + .bui-HeaderTop[data-sticky] { padding-top: var(--bui-space-6); } @@ -50,10 +51,14 @@ flex-direction: row; justify-content: space-between; gap: var(--bui-space-3); - padding-top: var(--bui-space-2); + padding-top: var(--bui-space-6); padding-bottom: var(--bui-space-3); } + .bui-HeaderContent[data-has-tags] { + padding-top: var(--bui-space-2); + } + .bui-HeaderContent[data-sticky] { position: sticky; top: 0; diff --git a/packages/ui/src/components/Header/Header.tsx b/packages/ui/src/components/Header/Header.tsx index ca1d43c776..56f43157a3 100644 --- a/packages/ui/src/components/Header/Header.tsx +++ b/packages/ui/src/components/Header/Header.tsx @@ -99,6 +99,7 @@ export const Header = (props: HeaderProps) => { () => (description ? renderInlineMarkdown(description) : null), [description], ); + // The sentinel sits directly before the sticky content and leaves the // viewport when the content becomes stuck, letting us toggle stuck styling. const stickySentinelRef = useRef(null); @@ -149,131 +150,35 @@ export const Header = (props: HeaderProps) => { }; }, [sticky]); - const beforeStickyContent = tags && tags.length > 0 && ( -
-
    - {tags.map((tag, i) => ( -
  • - {tag.href ? ( - - {tag.label} - - ) : ( - - {tag.label} - - )} -
  • - ))} -
-
- ); - - const titleAndActionsContent = ( - <> -
- {isStuck ? ( -
- {breadcrumbs && - breadcrumbs.map(breadcrumb => ( - - - {breadcrumb.label} - - - - ))} -

{title}

-
- ) : ( -
- {breadcrumbs && - breadcrumbs.map(breadcrumb => ( - - - {breadcrumb.label} - - - - ))} -

{title}

-
- )} -
-
{customActions}
- - ); - - const afterStickyContent = (description || - (metadata && metadata.length > 0) || - tabs) && ( -
- {description && ( - - {descriptionNodes} - - )} - {metadata && metadata.length > 0 && ( -
- {metadata.map((item, i) => ( -
-
- - {item.label} - -
-
- {typeof item.value === 'string' ? ( - {item.value} - ) : ( - item.value - )} -
-
- ))} -
- )} - {tabs && ( -
- -
- )} -
- ); - return ( <> - {beforeStickyContent} + {tags && tags.length > 0 && ( +
+
    + {tags.map((tag, i) => ( +
  • + {tag.href ? ( + + {tag.label} + + ) : ( + + {tag.label} + + )} +
  • + ))} +
+
+ )} {sticky && (
{ aria-hidden="true" /> )} -
0 ? '' : undefined} {...dataAttributes} > - {titleAndActionsContent} -
- {afterStickyContent} +
+ {isStuck ? ( +
+ {breadcrumbs && + breadcrumbs.map(breadcrumb => ( + + + {breadcrumb.label} + + + + ))} +

{title}

+
+ ) : ( +
+ {breadcrumbs && + breadcrumbs.map(breadcrumb => ( + + + {breadcrumb.label} + + + + ))} +

{title}

+
+ )} +
+
{customActions}
+
+
+ {description && ( + + {descriptionNodes} + + )} + {metadata && metadata.length > 0 && ( +
+ {metadata.map((item, i) => ( +
+
+ + {item.label} + +
+
+ {typeof item.value === 'string' ? ( + {item.value} + ) : ( + item.value + )} +
+
+ ))} +
+ )} + {tabs && ( +
+ +
+ )} +
); }; diff --git a/packages/ui/src/components/Header/definition.ts b/packages/ui/src/components/Header/definition.ts index 4811871094..010f641391 100644 --- a/packages/ui/src/components/Header/definition.ts +++ b/packages/ui/src/components/Header/definition.ts @@ -27,10 +27,10 @@ export const HeaderDefinition = defineComponent()({ bg: 'consumer', classNames: { root: 'bui-Header', - beforeSticky: 'bui-HeaderBeforeSticky', + headerTop: 'bui-HeaderTop', stickySentinel: 'bui-HeaderStickySentinel', content: 'bui-HeaderContent', - afterSticky: 'bui-HeaderAfterSticky', + headerBottom: 'bui-HeaderBottom', breadcrumbs: 'bui-HeaderBreadcrumbs', breadcrumbsSmall: 'bui-HeaderBreadcrumbsSmall', breadcrumbLink: 'bui-HeaderBreadcrumbLink', diff --git a/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx index a43252a9b7..623a4cf7b9 100644 --- a/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx +++ b/packages/ui/src/recipes/PluginHeaderAndHeader.stories.tsx @@ -157,52 +157,6 @@ export const WithTabs = meta.story({ ), }); -export const WithBreadcrumb = 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" - /> - - } - /> -
- - - - } - /> - - - ), -}); - const subTabs: HeaderNavTabItem[] = [ { id: 'summary', label: 'Summary', href: '/summary' }, { id: 'steps', label: 'Steps', href: '/steps' }, @@ -244,11 +198,65 @@ export const WithSubTabs = meta.story({ } />
+ + + + } + /> + + + ); + }, +}); + +export const WithSubTabsAndTags = meta.story({ + decorators: [ + (Story: StoryFn) => ( + + + + + + ), + ], + render: () => { + return ( + <> + } + 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" + /> + + } + /> +