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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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={
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user