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 <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-04-26 20:36:24 +01:00
parent 0e71545ee6
commit 8a0a5c9553
6 changed files with 193 additions and 193 deletions
+3 -4
View File
@@ -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);
}
}
+4 -4
View File
@@ -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';
@@ -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;
+115 -127
View File
@@ -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<HTMLDivElement>(null);
@@ -149,131 +150,35 @@ export const Header = (props: HeaderProps) => {
};
}, [sticky]);
const beforeStickyContent = tags && tags.length > 0 && (
<div className={classes.beforeSticky} data-sticky={sticky || undefined}>
<ul className={classes.tags}>
{tags.map((tag, i) => (
<li
key={`${i}:${tag.label}:${tag.href ?? ''}`}
className={classes.tag}
>
{tag.href ? (
<Link
href={tag.href}
variant="body-medium"
color="secondary"
standalone
>
{tag.label}
</Link>
) : (
<Text variant="body-medium" color="secondary">
{tag.label}
</Text>
)}
</li>
))}
</ul>
</div>
);
const titleAndActionsContent = (
<>
<div className={classes.titleStack}>
{isStuck ? (
<div className={classes.breadcrumbsSmall}>
{breadcrumbs &&
breadcrumbs.map(breadcrumb => (
<Fragment key={breadcrumb.label}>
<Link
href={breadcrumb.href}
color="secondary"
className={classes.breadcrumbLinkSmall}
standalone
>
{breadcrumb.label}
</Link>
<RiArrowRightSLine
className={classes.breadcrumbSeparator}
size={16}
color="var(--bui-fg-secondary)"
/>
</Fragment>
))}
<h2 className={classes.titleSmall}>{title}</h2>
</div>
) : (
<div className={classes.breadcrumbs}>
{breadcrumbs &&
breadcrumbs.map(breadcrumb => (
<Fragment key={breadcrumb.label}>
<Link
href={breadcrumb.href}
color="secondary"
className={classes.breadcrumbLink}
standalone
>
{breadcrumb.label}
</Link>
<RiArrowRightSLine
className={classes.breadcrumbSeparator}
size={16}
color="var(--bui-fg-secondary)"
/>
</Fragment>
))}
<h2 className={classes.title}>{title}</h2>
</div>
)}
</div>
<div className={classes.controls}>{customActions}</div>
</>
);
const afterStickyContent = (description ||
(metadata && metadata.length > 0) ||
tabs) && (
<div className={classes.afterSticky} data-sticky={sticky || undefined}>
{description && (
<Text
variant="body-medium"
color="secondary"
className={classes.description}
>
{descriptionNodes}
</Text>
)}
{metadata && metadata.length > 0 && (
<dl className={classes.metaRow}>
{metadata.map((item, i) => (
<div key={`${i}:${item.label}`} className={classes.metaItem}>
<dt>
<Text variant="body-medium" color="secondary">
{item.label}
</Text>
</dt>
<dd>
{typeof item.value === 'string' ? (
<Text variant="body-medium">{item.value}</Text>
) : (
item.value
)}
</dd>
</div>
))}
</dl>
)}
{tabs && (
<div className={classes.tabsWrapper}>
<HeaderNav tabs={tabs} activeTabId={activeTabId} />
</div>
)}
</div>
);
return (
<>
{beforeStickyContent}
{tags && tags.length > 0 && (
<div className={classes.headerTop} data-sticky={sticky || undefined}>
<ul className={classes.tags}>
{tags.map((tag, i) => (
<li
key={`${i}:${tag.label}:${tag.href ?? ''}`}
className={classes.tag}
>
{tag.href ? (
<Link
href={tag.href}
variant="body-medium"
color="secondary"
standalone
>
{tag.label}
</Link>
) : (
<Text variant="body-medium" color="secondary">
{tag.label}
</Text>
)}
</li>
))}
</ul>
</div>
)}
{sticky && (
<div
ref={stickySentinelRef}
@@ -282,15 +187,98 @@ export const Header = (props: HeaderProps) => {
aria-hidden="true"
/>
)}
<header
<div
className={classes.content}
data-sticky={sticky || undefined}
data-stuck={isStuck || undefined}
data-has-tags={tags && tags.length > 0 ? '' : undefined}
{...dataAttributes}
>
{titleAndActionsContent}
</header>
{afterStickyContent}
<div className={classes.titleStack}>
{isStuck ? (
<div className={classes.breadcrumbsSmall}>
{breadcrumbs &&
breadcrumbs.map(breadcrumb => (
<Fragment key={breadcrumb.label}>
<Link
href={breadcrumb.href}
color="secondary"
className={classes.breadcrumbLinkSmall}
standalone
>
{breadcrumb.label}
</Link>
<RiArrowRightSLine
className={classes.breadcrumbSeparator}
size={16}
color="var(--bui-fg-secondary)"
/>
</Fragment>
))}
<h2 className={classes.titleSmall}>{title}</h2>
</div>
) : (
<div className={classes.breadcrumbs}>
{breadcrumbs &&
breadcrumbs.map(breadcrumb => (
<Fragment key={breadcrumb.label}>
<Link
href={breadcrumb.href}
color="secondary"
className={classes.breadcrumbLink}
standalone
>
{breadcrumb.label}
</Link>
<RiArrowRightSLine
className={classes.breadcrumbSeparator}
size={16}
color="var(--bui-fg-secondary)"
/>
</Fragment>
))}
<h2 className={classes.title}>{title}</h2>
</div>
)}
</div>
<div className={classes.controls}>{customActions}</div>
</div>
<div className={classes.headerBottom} data-sticky={sticky || undefined}>
{description && (
<Text
variant="body-medium"
color="secondary"
className={classes.description}
>
{descriptionNodes}
</Text>
)}
{metadata && metadata.length > 0 && (
<dl className={classes.metaRow}>
{metadata.map((item, i) => (
<div key={`${i}:${item.label}`} className={classes.metaItem}>
<dt>
<Text variant="body-medium" color="secondary">
{item.label}
</Text>
</dt>
<dd>
{typeof item.value === 'string' ? (
<Text variant="body-medium">{item.value}</Text>
) : (
item.value
)}
</dd>
</div>
))}
</dl>
)}
{tabs && (
<div className={classes.tabsWrapper}>
<HeaderNav tabs={tabs} activeTabId={activeTabId} />
</div>
)}
</div>
</>
);
};
@@ -27,10 +27,10 @@ export const HeaderDefinition = defineComponent<HeaderOwnProps>()({
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',
@@ -157,52 +157,6 @@ export const WithTabs = meta.story({
),
});
export const WithBreadcrumb = 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="secondary"
icon={<RiRefreshLine />}
aria-label="Refresh"
/>
</>
}
/>
<Header
title="main · #842"
breadcrumbs={[
{ label: 'Catalog', href: '/catalog' },
{ label: 'Services', href: '/catalog?kind=Component' },
]}
customActions={
<>
<Button variant="secondary" iconStart={<RiDownloadLine />}>
Download logs
</Button>
<Button variant="primary" iconStart={<RiPlayLine />}>
Re-run pipeline
</Button>
</>
}
/>
<PageContent />
</>
),
});
const subTabs: HeaderNavTabItem[] = [
{ id: 'summary', label: 'Summary', href: '/summary' },
{ id: 'steps', label: 'Steps', href: '/steps' },
@@ -244,11 +198,65 @@ export const WithSubTabs = meta.story({
}
/>
<Header
title="main · #842"
title="Production deployment pipeline run"
tabs={subTabs}
breadcrumbs={[
{ label: 'Catalog', href: '/catalog' },
{ label: 'Services', href: '/catalog?kind=Component' },
customActions={
<>
<Button variant="secondary" iconStart={<RiDownloadLine />}>
Download logs
</Button>
<Button variant="primary" iconStart={<RiPlayLine />}>
Re-run pipeline
</Button>
</>
}
/>
<PageContent />
</>
);
},
});
export const WithSubTabsAndTags = meta.story({
decorators: [
(Story: StoryFn) => (
<MemoryRouter initialEntries={['/summary']}>
<BUIProvider>
<Story />
</BUIProvider>
</MemoryRouter>
),
],
render: () => {
return (
<>
<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="secondary"
icon={<RiRefreshLine />}
aria-label="Refresh"
/>
</>
}
/>
<Header
title="Production deployment pipeline run"
tabs={subTabs}
tags={[
{ label: 'Production' },
{ label: 'Owner: Platform', href: '/catalog?owner=platform' },
{ label: 'Tier 1' },
]}
customActions={
<>