Improve design

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-01-26 12:56:30 +00:00
parent d1940015a9
commit f0943b0ce4
6 changed files with 239 additions and 225 deletions
@@ -18,35 +18,28 @@
@layer components {
.bui-Alert {
--loading-duration: 200ms;
--alert-bg: var(--bui-bg-surface-1);
--alert-fg: var(--bui-fg-primary);
--alert-border: var(--bui-border);
position: relative;
display: flex;
align-items: flex-start;
gap: var(--bui-space-3);
padding: var(--bui-space-3) var(--bui-space-4);
gap: var(--bui-space-2);
padding: var(--bui-space-2) var(--bui-space-3);
border-radius: var(--bui-radius-3);
border: 1px solid var(--alert-border);
font-family: var(--bui-font-regular);
font-size: var(--bui-font-size-3);
line-height: 1.5;
transition: opacity var(--loading-duration) ease-out;
/* Apply variables */
background-color: var(--alert-bg);
color: var(--alert-fg);
&[data-loading='true'] {
opacity: 0.7;
}
}
.bui-Alert[data-status='info'] {
--alert-bg: var(--bui-bg-surface-1);
--alert-fg: var(--bui-fg-primary);
--alert-bg: var(--bui-bg-info);
--alert-fg: var(--bui-fg-info);
--alert-border: var(--bui-border-info);
}
@@ -74,15 +67,10 @@
align-items: center;
justify-content: center;
margin-top: 0.125rem;
transition: opacity var(--loading-duration) ease-out;
svg {
width: 1.25rem;
height: 1.25rem;
}
.bui-Alert[data-loading='true'] & {
opacity: 0;
width: 1rem;
height: 1rem;
}
}
@@ -92,21 +80,22 @@
word-wrap: break-word;
}
.bui-AlertSpinner {
position: absolute;
top: var(--bui-space-3);
left: var(--bui-space-4);
display: flex;
opacity: 0;
transition: opacity var(--loading-duration) ease-in;
.bui-AlertTitle {
font-weight: var(--bui-font-weight-bold);
font-size: var(--bui-font-size-3);
}
.bui-Alert[data-loading='true'] & {
opacity: 1;
}
.bui-AlertDescription {
font-size: var(--bui-font-size-2);
opacity: 0.9;
}
.bui-AlertSpinner {
display: flex;
& svg {
width: 1.25rem;
height: 1.25rem;
width: 1rem;
height: 1rem;
animation: bui-spin 1s linear infinite;
}
}
@@ -120,18 +109,6 @@
}
@media (prefers-reduced-motion: reduce) {
.bui-Alert {
transition-duration: 50ms;
}
.bui-AlertIcon {
transition-duration: 50ms;
}
.bui-AlertSpinner {
transition-duration: 50ms;
}
.bui-AlertSpinner svg {
animation: none;
}
+172 -147
View File
@@ -20,7 +20,6 @@ import { Box } from '../Box';
import { Text } from '../Text';
import { Button } from '../Button';
import { RiCloudLine } from '@remixicon/react';
import { useState } from 'react';
const meta = preview.meta({
title: 'Backstage UI/Alert',
@@ -41,14 +40,14 @@ const meta = preview.meta({
export const Default = meta.story({
args: {
children: 'This is an alert message',
title: 'This is an alert message',
icon: true,
},
});
export const StatusVariants = meta.story({
args: {
children: 'This is an alert message',
title: 'This is an alert message',
},
parameters: {
argTypes: {
@@ -59,18 +58,57 @@ export const StatusVariants = meta.story({
},
render: () => (
<Flex direction="column" gap="4">
<Alert status="info" icon={true}>
This is an informational alert with helpful information.
</Alert>
<Alert status="success" icon={true}>
Your changes have been saved successfully.
</Alert>
<Alert status="warning" icon={true}>
This action may have unintended consequences.
</Alert>
<Alert status="danger" icon={true}>
An error occurred while processing your request.
</Alert>
<Alert
status="info"
icon={true}
title="This is an informational alert with helpful information."
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved successfully."
/>
<Alert
status="warning"
icon={true}
title="This action may have unintended consequences."
/>
<Alert
status="danger"
icon={true}
title="An error occurred while processing your request."
/>
</Flex>
),
});
export const WithDescription = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
title="New Feature Available"
description="We've added support for custom table columns. Check the documentation to learn more."
/>
<Alert
status="success"
icon={true}
title="Deployment Successful"
description="Your application has been deployed to production. All health checks passed."
/>
<Alert
status="warning"
icon={true}
title="Pending Review"
description="Please review the following items before proceeding with the deployment."
/>
<Alert
status="danger"
icon={true}
title="Authentication Failed"
description="Unable to verify your credentials. Please check your username and password and try again."
/>
</Flex>
),
});
@@ -78,18 +116,26 @@ export const StatusVariants = meta.story({
export const WithoutIcons = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert status="info" icon={false}>
This is an informational alert without an icon.
</Alert>
<Alert status="success" icon={false}>
Your changes have been saved successfully.
</Alert>
<Alert status="warning" icon={false}>
This action may have unintended consequences.
</Alert>
<Alert status="danger" icon={false}>
An error occurred while processing your request.
</Alert>
<Alert
status="info"
icon={false}
title="This is an informational alert without an icon."
/>
<Alert
status="success"
icon={false}
title="Your changes have been saved successfully."
/>
<Alert
status="warning"
icon={false}
title="This action may have unintended consequences."
/>
<Alert
status="danger"
icon={false}
title="An error occurred while processing your request."
/>
</Flex>
),
});
@@ -97,12 +143,16 @@ export const WithoutIcons = meta.story({
export const CustomIcon = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert status="info" icon={<RiCloudLine />}>
This alert uses a custom cloud icon instead of the default info icon.
</Alert>
<Alert status="success" icon={<RiCloudLine />}>
Custom icons work with any status variant.
</Alert>
<Alert
status="info"
icon={<RiCloudLine />}
title="This alert uses a custom cloud icon instead of the default info icon."
/>
<Alert
status="success"
icon={<RiCloudLine />}
title="Custom icons work with any status variant."
/>
</Flex>
),
});
@@ -113,19 +163,17 @@ export const WithActions = meta.story({
<Alert
status="info"
icon={true}
title="This alert has a dismiss action on the right."
customActions={
<>
<Button size="small" variant="tertiary">
Dismiss
</Button>
</>
<Button size="small" variant="tertiary">
Dismiss
</Button>
}
>
This alert has a dismiss action on the right.
</Alert>
/>
<Alert
status="success"
icon={true}
title="Your changes have been saved. Would you like to continue?"
customActions={
<>
<Button size="small" variant="tertiary">
@@ -136,78 +184,71 @@ export const WithActions = meta.story({
</Button>
</>
}
>
Your changes have been saved. Would you like to continue?
</Alert>
/>
<Alert
status="danger"
icon={true}
title="An error occurred while processing your request. Please try again."
customActions={
<>
<Button size="small" variant="primary">
Retry
</Button>
</>
<Button size="small" variant="primary">
Retry
</Button>
}
>
An error occurred while processing your request. Please try again.
</Alert>
/>
</Flex>
),
});
export const Loading = meta.story({
render: () => {
const [isLoading, setIsLoading] = useState(false);
const handleLoad = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
}, 3000);
};
return (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
loading={isLoading}
customActions={
<Button size="small" variant="primary" onPress={handleLoad}>
Load
</Button>
}
>
Click the button to see the loading state
</Alert>
</Flex>
);
},
});
export const LoadingVariants = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Text>Info</Text>
<Alert status="info" icon={true} loading>
Processing your request...
</Alert>
<Alert
status="info"
icon={true}
loading
title="Processing your request..."
/>
<Text>Success</Text>
<Alert status="success" icon={true} loading>
Saving changes...
</Alert>
<Alert status="success" icon={true} loading title="Saving changes..." />
<Text>Warning</Text>
<Alert status="warning" icon={true} loading>
Checking for issues...
</Alert>
<Alert
status="warning"
icon={true}
loading
title="Checking for issues..."
/>
<Text>Danger</Text>
<Alert status="danger" icon={true} loading>
Attempting recovery...
</Alert>
<Alert
status="danger"
icon={true}
loading
title="Attempting recovery..."
/>
</Flex>
),
});
export const LoadingWithDescription = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert
status="info"
icon={true}
loading
title="Processing your request"
description="This may take a few moments. Please do not close this window."
/>
<Alert
status="success"
icon={true}
loading
title="Deployment in Progress"
description="Your application is being deployed to production. You'll receive a notification when complete."
/>
</Flex>
),
});
@@ -215,26 +256,21 @@ export const LoadingVariants = meta.story({
export const LongContent = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert status="info" icon={true}>
This is a longer alert message that demonstrates how the component
handles multiple lines of text. The content will wrap naturally and
maintain proper spacing with the icon and any actions. This is useful
for providing detailed information to users when necessary.
</Alert>
<Alert
status="info"
icon={true}
title="This is a longer alert message that demonstrates how the component handles multiple lines of text. The content will wrap naturally and maintain proper spacing with the icon and any actions. This is useful for providing detailed information to users when necessary."
/>
<Alert
status="warning"
icon={true}
title="This alert combines long content with actions. The actions remain aligned to the right even when the content wraps to multiple lines. This ensures a consistent and predictable layout regardless of content length."
customActions={
<Button size="small" variant="tertiary">
Dismiss
</Button>
}
>
This alert combines long content with actions. The actions remain
aligned to the right even when the content wraps to multiple lines. This
ensures a consistent and predictable layout regardless of content
length.
</Alert>
/>
</Flex>
),
});
@@ -245,60 +281,44 @@ export const OnDifferentSurfaces = meta.story({
<Flex direction="column" gap="4">
<Text>Default Surface</Text>
<Flex direction="column" gap="2" p="4">
<Alert status="info" icon={true}>
Alert on default surface
</Alert>
<Alert status="success" icon={true}>
Alert on default surface
</Alert>
<Alert status="info" icon={true} title="Alert on default surface" />
<Alert
status="success"
icon={true}
title="Alert on default surface"
/>
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 0</Text>
<Flex direction="column" gap="2" surface="0" p="4">
<Alert status="info" icon={true}>
Alert on surface 0
</Alert>
<Alert status="success" icon={true}>
Alert on surface 0
</Alert>
<Alert status="info" icon={true} title="Alert on surface 0" />
<Alert status="success" icon={true} title="Alert on surface 0" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 1</Text>
<Flex direction="column" gap="2" surface="1" p="4">
<Alert status="info" icon={true}>
Alert on surface 1
</Alert>
<Alert status="success" icon={true}>
Alert on surface 1
</Alert>
<Alert status="info" icon={true} title="Alert on surface 1" />
<Alert status="success" icon={true} title="Alert on surface 1" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 2</Text>
<Flex direction="column" gap="2" surface="2" p="4">
<Alert status="info" icon={true}>
Alert on surface 2
</Alert>
<Alert status="success" icon={true}>
Alert on surface 2
</Alert>
<Alert status="info" icon={true} title="Alert on surface 2" />
<Alert status="success" icon={true} title="Alert on surface 2" />
</Flex>
</Flex>
<Flex direction="column" gap="4">
<Text>On Surface 3</Text>
<Flex direction="column" gap="2" surface="3" p="4">
<Alert status="info" icon={true}>
Alert on surface 3
</Alert>
<Alert status="success" icon={true}>
Alert on surface 3
</Alert>
<Alert status="info" icon={true} title="Alert on surface 3" />
<Alert status="success" icon={true} title="Alert on surface 3" />
</Flex>
</Flex>
</Flex>
@@ -307,7 +327,7 @@ export const OnDifferentSurfaces = meta.story({
export const Responsive = meta.story({
args: {
children: 'This alert changes status responsively',
title: 'This alert changes status responsively',
icon: true,
status: {
initial: 'info',
@@ -320,15 +340,20 @@ export const Responsive = meta.story({
export const WithUtilityProps = meta.story({
render: () => (
<Flex direction="column" gap="4">
<Alert status="info" icon={true} mb="4" p="5">
Alert with custom margin and padding using utility props
</Alert>
<Box surface="1" p="4">
<Alert status="success" icon={true} mb="0">
Alert with zero margin bottom
</Alert>
</Box>
</Flex>
<Box surface="1" py="4">
<Alert
status="success"
icon={true}
title="Alert with custom margin"
mb="4"
mx="4"
/>
<Alert
status="success"
icon={true}
title="Alert with custom margin"
mx="4"
/>
</Box>
),
});
+37 -18
View File
@@ -36,9 +36,20 @@ import { AlertDefinition } from './definition';
* icon selection based on status when the icon prop is set to true.
*
* @example
* Basic usage:
* Basic usage with title only:
* ```tsx
* <Alert status="info">This is an informational message</Alert>
* <Alert status="info" title="This is an informational message" />
* ```
*
* @example
* With title and description:
* ```tsx
* <Alert
* status="warning"
* icon={true}
* title="Pending Review"
* description="Please review the following items before proceeding."
* />
* ```
*
* @example
@@ -47,16 +58,16 @@ import { AlertDefinition } from './definition';
* <Alert
* status="success"
* icon={true}
* title="Operation completed"
* description="Your changes have been saved successfully."
* loading={isProcessing}
* customActions={
* <>
* <Button size="small" variant="tertiary">Dismiss</Button>
* <Button size="small" variant="primary">Action</Button>
* <Button size="small" variant="primary">View</Button>
* </>
* }
* >
* Operation completed successfully
* </Alert>
* />
* ```
*
* @public
@@ -73,8 +84,9 @@ export const Alert = forwardRef(
icon,
loading,
customActions,
title,
description,
style,
surfaceChildren: children,
} = ownProps;
// Determine which icon to render
@@ -118,19 +130,26 @@ export const Alert = forwardRef(
{...dataAttributes}
{...restProps}
>
{statusIcon && <div className={classes.icon}>{statusIcon}</div>}
{loading && (
<ProgressBar
aria-label="Loading"
isIndeterminate
className={classes.spinner}
>
<RiLoader4Line aria-hidden="true" />
</ProgressBar>
{loading ? (
<div className={classes.icon}>
<ProgressBar
aria-label="Loading"
isIndeterminate
className={classes.spinner}
>
<RiLoader4Line aria-hidden="true" />
</ProgressBar>
</div>
) : (
statusIcon && <div className={classes.icon}>{statusIcon}</div>
)}
<div className={classes.content}>{children}</div>
<div className={classes.content}>
{title && <div className={classes.title}>{title}</div>}
{description && (
<div className={classes.description}>{description}</div>
)}
</div>
{customActions && (
<div className={classes.actions}>{customActions}</div>
+5 -17
View File
@@ -27,6 +27,8 @@ export const AlertDefinition = defineComponent<AlertOwnProps>()({
classNames: {
root: 'bui-Alert',
content: 'bui-AlertContent',
title: 'bui-AlertTitle',
description: 'bui-AlertDescription',
icon: 'bui-AlertIcon',
spinner: 'bui-AlertSpinner',
actions: 'bui-AlertActions',
@@ -37,25 +39,11 @@ export const AlertDefinition = defineComponent<AlertOwnProps>()({
loading: { dataAttribute: true },
icon: {},
customActions: {},
title: {},
description: {},
surface: {},
children: {},
className: {},
style: {},
},
utilityProps: [
'm',
'mb',
'ml',
'mr',
'mt',
'mx',
'my',
'p',
'pb',
'pl',
'pr',
'pt',
'px',
'py',
],
utilityProps: ['m', 'mb', 'ml', 'mr', 'mt', 'mx', 'my'],
});
+2 -1
View File
@@ -27,7 +27,8 @@ export type AlertOwnProps = ContainerSurfaceProps & {
icon?: boolean | ReactElement;
loading?: boolean;
customActions?: ReactNode;
children?: ReactNode;
title?: ReactNode;
description?: ReactNode;
className?: string;
style?: CSSProperties;
};
+4
View File
@@ -114,6 +114,7 @@
--bui-bg-neutral-on-surface-3-disabled: oklch(0% 0 0 / 6%);
/* Status background colors */
--bui-bg-info: #e0f2fe;
--bui-bg-danger: #feebe7;
--bui-bg-warning: #fff2b2;
--bui-bg-success: #e6f6eb;
@@ -128,6 +129,7 @@
--bui-fg-solid-disabled: #98a8bc;
--bui-fg-tint: #1f5493;
--bui-fg-tint-disabled: var(--bui-gray-5);
--bui-fg-info: #0369a1;
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;
@@ -200,6 +202,7 @@
--bui-bg-neutral-on-surface-3-disabled: oklch(100% 0 0 / 8%);
/* Status background colors */
--bui-bg-info: #082f49;
--bui-bg-danger: #3b1219;
--bui-bg-warning: #302008;
--bui-bg-success: #132d21;
@@ -214,6 +217,7 @@
--bui-fg-solid-disabled: #6191cc;
--bui-fg-tint: #9cc9ff;
--bui-fg-tint-disabled: var(--bui-gray-5);
--bui-fg-info: #7dd3fc;
--bui-fg-danger: #e22b2b;
--bui-fg-warning: #e36d05;
--bui-fg-success: #1db954;