Skip to content

Commit 698c573

Browse files
authored
fix(Alert): [DSYS-132] Rollback inline Alert styles and add new actions layout prop (#1885)
* fix(components): rollback inline Alert styles, add actionsLayout prop Roll back inline Alert variant to minimal styling after Meticulous tests revealed issues with the new styles when alerts are used truly "inline" alongside other elements. Changes: - Restore minimal styles for inline Alert variant (no background/border) - Keep new block variant styles with colored backgrounds and borders - Add actionsLayout prop ('stacked' | 'inline') for block variant to control whether ButtonGroup appears below content or on the right - Automatically wrap non-ButtonGroup children in a container to keep heading and text stacked together when using inline actions layout - Remove inlineAction slot and ButtonContext for inline variant - Restrict hideIcon prop to block variant only - Add displayName to ButtonGroup component to support runtime detection when separating children in Alert Breaking changes: None - existing usage continues to work as expected. Made-with: Cursor * docs(components): reorganize Alert stories into Block and Inline sections Restructure Alert stories for better organization and discoverability. - Add nested story names (Block/*, Inline/*) to group related examples - Consolidate individual color stories into "Tones" stories - Add "Without Header" story showing inline actions variations - Add actionsLayout to Storybook argTypes whitelist Made-with: Cursor * style(components): make padding & positioning more consistent between stacked and inline actions * style(components): clean up CSS * fix(Alert): use absolute positioning for dismiss button to prevent extra height - Changed .default .close from position: relative to position: absolute - Added conditional right padding via :has(.close) to prevent text overlap - The actionsInline variant resets to relative positioning at wider container widths to maintain inline flow - Wrapped Alert content in container div for CSS container queries - Added isDismissable to info alert in BlockTones story for testing Made-with: Cursor * refactor(Alert): replace automatic children parsing with AlertText wrapper - Removed isButtonGroup, flattenChildren, separateChildren helper functions - Added AlertText component for grouping title/description in actionsLayout="inline" - Simplified Alert to render children directly without auto-wrapping - Updated stories to use AlertText wrapper for inline actions layouts - Stacked layouts remain backward compatible (no wrapper needed) Made-with: Cursor * fix(components): code cleanup * fix(Alert): remove wrapper div, apply container-type to alert element - Moved container-type: inline-size from wrapper to .default class - Removed unnecessary container wrapper div - Put ref and props on same element as role="alert" (fixes ref behavior) - Moved vertical padding to .content to enable container query modifications - Changed default status from 'neutral' to 'info' * fix(Alert): roll back to neutral default variant * fix(Alert): remove unused CSS class actionsStacked * fix(Alert): address cursor and devin issues * chore(Alert): add changeset * fix(Alert): restore hideIcon behavior for all variants The hideIcon prop was incorrectly only being respected for the default variant due to inverted logic. This restores the original behavior where hideIcon works for both block and inline variants. Made-with: Cursor * feat(Alert): add Neutral story for overview page example Add a dedicated Neutral story with args format that can be used by the Status overview page, and update the import in Status.tsx to use it. Made-with: Cursor * fix(Alert): remove container query, apply actionsInline styles unconditionally Remove responsive container query behavior to simplify the implementation. The actionsLayout="inline" prop now always applies inline styles. Responsive behavior can be added in a follow-up PR. Made-with: Cursor * refactor(Alert): improve CSS readability with comments and simplified values * refactor(Alert): improve CSS readability and remove accidental files - Replace calc() with var(--lp-size-48) for close button padding - Use unset for actionsInline overrides instead of explicit values - Add descriptive comments throughout Alert.module.css - Remove accidentally committed screenshot files Made-with: Cursor * chore(changeset): update Alert changeset description * fix(Alert): adjust right padding when no dismiss button is present * chore(Alert): update copy on stories
1 parent 64cccc5 commit 698c573

7 files changed

Lines changed: 283 additions & 214 deletions

File tree

.changeset/clever-walls-switch.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@launchpad-ui/components": minor
3+
---
4+
5+
Add AlertText component and actionsLayout prop for block variant
6+
7+
- Add new `AlertText` component to wrap heading and description content
8+
- Add `actionsLayout` prop with "stacked" (default) and "inline" options for block variant
9+
- Add `hideIcon` prop to hide the status icon
10+
- Refactor block variant styles with new color system and layout improvements
11+
- Reorganize Storybook stories with clearer naming (Block/* and Inline/*)
12+
- Export AlertText and AlertTextProps from components package

.storybook/preview.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ const enhanceArgTypes = (context: any) => {
269269
'appearance',
270270
'position',
271271
'placement',
272+
'actionsLayout',
272273
];
273274

274275
if (componentProps.includes(key)) {
@@ -326,6 +327,7 @@ const enhanceArgTypes = (context: any) => {
326327
'appearance',
327328
'position',
328329
'placement',
330+
'actionsLayout',
329331
];
330332

331333
if (componentProps.includes(key)) {

packages/components/src/Alert.tsx

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
import type { VariantProps } from 'class-variance-authority';
2-
import type { HTMLAttributes, Ref } from 'react';
2+
import type { ComponentProps, HTMLAttributes, Ref } from 'react';
33

44
import { StatusIcon } from '@launchpad-ui/icons';
55
import { useControlledState } from '@react-stately/utils';
66
import { cva } from 'class-variance-authority';
77
import { HeadingContext, Provider } from 'react-aria-components';
88

9-
import { ButtonContext } from './Button';
109
import { ButtonGroupContext } from './ButtonGroup';
1110
import { IconButton } from './IconButton';
1211
import styles from './styles/Alert.module.css';
1312

13+
interface AlertTextProps extends ComponentProps<'div'> {
14+
ref?: Ref<HTMLDivElement>;
15+
}
16+
17+
/**
18+
* AlertText wraps the title and description content within an Alert.
19+
* Use this to group Heading and Text elements when using actionsLayout="inline".
20+
*/
21+
const AlertText = ({ className, ref, ...props }: AlertTextProps) => {
22+
return <div ref={ref} className={`${styles.text} ${className ?? ''}`.trim()} {...props} />;
23+
};
24+
1425
const alertStyles = cva(styles.base, {
1526
variants: {
1627
status: {
@@ -24,6 +35,10 @@ const alertStyles = cva(styles.base, {
2435
default: styles.default,
2536
inline: styles.inline,
2637
},
38+
actionsLayout: {
39+
stacked: null,
40+
inline: styles.actionsInline,
41+
},
2742
},
2843
defaultVariants: {
2944
status: 'neutral',
@@ -34,6 +49,8 @@ const alertStyles = cva(styles.base, {
3449
interface AlertVariants extends VariantProps<typeof alertStyles> {}
3550

3651
interface AlertProps extends HTMLAttributes<HTMLDivElement>, AlertVariants {
52+
/** Controls the layout of actions within the alert (block variant only). */
53+
actionsLayout?: 'stacked' | 'inline';
3754
/** Hides the status icon. */
3855
hideIcon?: boolean;
3956
/** Whether the alert can be dismissed. */
@@ -50,6 +67,7 @@ const Alert = ({
5067
children,
5168
status = 'neutral',
5269
variant = 'default',
70+
actionsLayout = 'stacked',
5371
isDismissable,
5472
isOpen,
5573
onDismiss,
@@ -59,31 +77,27 @@ const Alert = ({
5977
}: AlertProps) => {
6078
const [open, setOpen] = useControlledState(isOpen, true, (val) => !val && onDismiss?.());
6179

80+
const showIcon = !hideIcon && status !== 'neutral';
81+
const resolvedActionsLayout = variant === 'default' ? actionsLayout : undefined;
82+
6283
return open ? (
63-
<div ref={ref} {...props} role="alert" className={alertStyles({ status, variant, className })}>
64-
{!hideIcon && status !== 'neutral' && (
65-
<StatusIcon size="small" kind={status || 'info'} className={styles.icon} />
66-
)}
84+
<div
85+
ref={ref}
86+
{...props}
87+
role="alert"
88+
className={alertStyles({
89+
status,
90+
variant,
91+
actionsLayout: resolvedActionsLayout,
92+
className,
93+
})}
94+
>
95+
{showIcon && <StatusIcon size="small" kind={status || 'info'} className={styles.icon} />}
6796
<div className={styles.content}>
6897
<Provider
6998
values={[
7099
[HeadingContext, { className: styles.heading }],
71-
[
72-
ButtonGroupContext,
73-
{
74-
className: styles.buttonGroup,
75-
},
76-
],
77-
[
78-
ButtonContext,
79-
variant === 'inline'
80-
? {
81-
className: styles.inlineAction,
82-
size: 'medium' as const,
83-
variant: 'default' as const,
84-
}
85-
: {},
86-
],
100+
[ButtonGroupContext, { className: styles.buttonGroup }],
87101
]}
88102
>
89103
{children}
@@ -103,5 +117,5 @@ const Alert = ({
103117
) : null;
104118
};
105119

106-
export { Alert, alertStyles };
107-
export type { AlertProps };
120+
export { Alert, AlertText, alertStyles };
121+
export type { AlertProps, AlertTextProps };

packages/components/src/ButtonGroup.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface ButtonGroupProps extends GroupProps, VariantProps<typeof buttonGroupSt
4040

4141
const ButtonGroupContext = createContext<ContextValue<ButtonGroupProps, HTMLDivElement>>(null);
4242

43-
const ButtonGroup = ({ ref, ...props }: ButtonGroupProps) => {
43+
const ButtonGroup = ({ ref, ...props }: ButtonGroupProps): React.JSX.Element => {
4444
[props, ref] = useLPContextProps(props, ref, ButtonGroupContext);
4545
const { spacing = 'basic', orientation = 'horizontal' } = props;
4646

@@ -67,5 +67,7 @@ const ButtonGroup = ({ ref, ...props }: ButtonGroupProps) => {
6767
);
6868
};
6969

70+
ButtonGroup.displayName = 'ButtonGroup';
71+
7072
export { ButtonGroup, ButtonGroupContext, buttonGroupStyles };
7173
export type { ButtonGroupProps };

packages/components/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './styles/base.css';
22
import './styles/themes.css';
33

4-
export type { AlertProps } from './Alert';
4+
export type { AlertProps, AlertTextProps } from './Alert';
55
export type { AutocompleteProps } from './Autocomplete';
66
export type { AvatarProps, InitialsAvatarProps } from './Avatar';
77
export type { BreadcrumbProps, BreadcrumbsProps } from './Breadcrumbs';
@@ -94,7 +94,7 @@ export type {
9494
TreeProps,
9595
} from './Tree';
9696

97-
export { Alert, alertStyles } from './Alert';
97+
export { Alert, AlertText, alertStyles } from './Alert';
9898
export { Autocomplete } from './Autocomplete';
9999
export { Avatar, avatarStyles, InitialsAvatar } from './Avatar';
100100
export {

packages/components/src/styles/Alert.module.css

Lines changed: 79 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@
2525
--alert-color-bg-warning: #3c170c;
2626
}
2727

28+
/* Shared styles for both the block and inline variants */
2829
.base {
2930
display: flex;
3031
border-radius: var(--lp-border-radius-medium);
3132
gap: var(--lp-spacing-300);
3233
align-items: flex-start;
3334

35+
/* Status icon styles */
3436
&.error .icon {
3537
fill: var(--lp-color-fill-feedback-error);
3638
}
@@ -48,14 +50,16 @@
4850
}
4951
}
5052

51-
.default,
52-
.inline {
53-
padding: var(--lp-spacing-500);
53+
/* Base styles for the block variant */
54+
.default {
55+
/* Add a base layer of padding on the Alert itself. This is just enough to create some breathing room in case any buttons are present. */
56+
padding: var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-500);
5457
background-color: var(--alert-color-bg-neutral);
5558
border: 1px solid var(--alert-color-border-neutral);
5659
position: relative;
5760
min-height: var(--lp-size-48);
5861

62+
/* Status borders and background colors */
5963
&.error {
6064
border-color: var(--alert-color-border-error);
6165
background-color: var(--alert-color-bg-error);
@@ -76,52 +80,76 @@
7680
background-color: var(--alert-color-bg-warning);
7781
}
7882

79-
&:has(.heading) {
80-
/* biome-ignore lint/style/noDescendingSpecificity: ignore */
81-
& .icon {
82-
transform: translateY(var(--lp-size-2));
83-
}
83+
& .icon {
84+
transform: translateY(var(--lp-size-10));
8485
}
8586

86-
/* Let's keep the close button in the default position, but position relative in case we want to move it later. */
87-
& .close {
88-
position: relative;
87+
/* Add another layer of vertical padding to the Alert content (text and actions). This will align the basline of the text with the baseline of the dismiss button. */
88+
.content {
89+
padding: var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-300) 0;
8990
}
90-
}
9191

92-
.inline {
93-
align-items: center;
94-
gap: var(--lp-spacing-300);
95-
padding: var(--lp-spacing-400) var(--lp-spacing-500);
92+
/* Add a little space at the top of the button group to separate it from the text */
93+
.buttonGroup {
94+
margin-top: var(--lp-spacing-400);
95+
}
9696

97-
& .close {
98-
margin-left: var(--lp-spacing-300);
97+
/* We need to position the close button absolutely so we can adjust its position without affecting the layout of the Alert content. */
98+
.close {
99+
position: absolute;
100+
top: var(--lp-spacing-300);
101+
right: var(--lp-spacing-300);
99102
}
100103

104+
/* Add extra right padding when there's a close button to prevent text from overlapping the button */
105+
&:has(.close) .content {
106+
padding-right: var(--lp-size-48);
107+
}
108+
}
109+
110+
/* Inline actions variant */
111+
.actionsInline {
112+
/* Switch the direction of the content to row and make sure it fills its container. */
101113
& .content {
102114
flex-direction: row;
103-
align-items: center;
104-
gap: var(--lp-spacing-400);
115+
align-items: flex-start;
116+
flex: 1;
117+
gap: var(--lp-spacing-500);
118+
119+
/* Unset padding on content to prevent it looking too big if there are actions present. */
120+
padding: unset;
105121
}
106122

107-
/* Adjust padding so the alert maintains the same height if there's an inline action or close button. We have to hardcode the vertical values because of the way borders get calculated as a part of an element's dimensions. */
108-
&:has(.inlineAction),
109-
&:has(.close) {
110-
padding: 7px var(--lp-spacing-300) 7px var(--lp-spacing-500);
123+
/* Move vertical padding from the content container to the text container. This ensures the text baseline is aligned with the trailing actions and dismiss buttons. We need to do some fancy calc stuff to make it look right. */
124+
& .text {
125+
padding: calc(var(--lp-spacing-300) - var(--lp-size-2)) 0;
111126
}
112127

113-
&:has(.inlineAction) {
114-
.close {
115-
margin-left: 0;
116-
}
128+
/* Need a specific selector to override the padding on the content container when there is a close button. */
129+
&:has(.close) .content {
130+
padding-right: unset;
131+
}
132+
133+
/* Adjust icon position to compensate for the different padding */
134+
& .icon {
135+
transform: translateY(var(--lp-spacing-300));
136+
}
137+
138+
/* Move the button group into position. We don't need a top margin here, and we want to flush it to the right */
139+
& .buttonGroup {
140+
margin-top: unset;
141+
margin-left: auto;
117142
}
118-
}
119143

120-
.inlineAction {
121-
flex-shrink: 0;
122-
margin-left: auto;
144+
/* Remove the absolute positioning from the close button so that it can participate in the flex layout. */
145+
& .close {
146+
position: unset;
147+
top: unset;
148+
right: unset;
149+
}
123150
}
124151

152+
/* Base styles for the content container */
125153
.content {
126154
min-width: 0;
127155
flex: 1;
@@ -131,21 +159,30 @@
131159
align-items: flex-start;
132160
}
133161

134-
.content .heading {
135-
font: var(--lp-text-body-2-semibold);
136-
}
137-
138-
.buttonGroup {
139-
margin-top: var(--lp-spacing-500);
162+
/* Base styles for the text container */
163+
.text {
164+
display: flex;
165+
flex-direction: column;
166+
flex: 1;
167+
min-width: 0;
140168
}
141169

142-
.default .close {
143-
bottom: var(--lp-spacing-300);
144-
left: var(--lp-spacing-300);
170+
/* Make sure the heading is the right size and weight regardless of which element is used */
171+
.heading {
172+
font: var(--lp-text-body-2-semibold) !important;
145173
}
146174

147-
/* Sometimes we want to make text bold, but we need to make sure this maps to the correct font weight. */
175+
/* Sometimes we want to make text bold, but we need to make sure this maps to semibold, not bold. */
148176
.content strong,
149177
.content b {
150178
font-weight: var(--lp-font-weight-semibold);
151179
}
180+
181+
/* Inline variant styles */
182+
.inline {
183+
align-items: center;
184+
185+
& .close {
186+
margin-left: var(--lp-spacing-300);
187+
}
188+
}

0 commit comments

Comments
 (0)