Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
5af2f29
fix(components): rollback inline Alert styles, add actionsLayout prop
cmwinters Apr 15, 2026
57dc241
docs(components): reorganize Alert stories into Block and Inline sect…
cmwinters Apr 15, 2026
e891f78
style(components): make padding & positioning more consistent between…
cmwinters Apr 15, 2026
9eaf069
style(components): clean up CSS
cmwinters Apr 15, 2026
addfd16
fix(Alert): use absolute positioning for dismiss button to prevent ex…
cmwinters Apr 16, 2026
50a545b
refactor(Alert): replace automatic children parsing with AlertText wr…
cmwinters Apr 16, 2026
e9f9fc6
fix(components): code cleanup
cmwinters Apr 16, 2026
0c70675
fix(Alert): remove wrapper div, apply container-type to alert element
cmwinters Apr 17, 2026
d786173
fix(Alert): roll back to neutral default variant
cmwinters Apr 17, 2026
50a2481
fix(Alert): remove unused CSS class actionsStacked
cmwinters Apr 17, 2026
44e7409
fix(Alert): address cursor and devin issues
cmwinters Apr 17, 2026
bf0217b
chore(Alert): add changeset
cmwinters Apr 17, 2026
970c2a7
fix(Alert): restore hideIcon behavior for all variants
cmwinters Apr 17, 2026
a01a383
feat(Alert): add Neutral story for overview page example
cmwinters Apr 17, 2026
a3f3e30
fix(Alert): remove container query, apply actionsInline styles uncond…
cmwinters Apr 17, 2026
95e1651
refactor(Alert): improve CSS readability with comments and simplified…
cmwinters Apr 17, 2026
5f7e492
refactor(Alert): improve CSS readability and remove accidental files
cmwinters Apr 17, 2026
bd15d37
chore(changeset): update Alert changeset description
cmwinters Apr 17, 2026
75e526c
fix(Alert): adjust right padding when no dismiss button is present
cmwinters Apr 17, 2026
bea5f05
chore(Alert): update copy on stories
cmwinters Apr 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/clever-walls-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@launchpad-ui/components": minor
---

Add AlertText component and actionsLayout prop for block variant

- Add new `AlertText` component to wrap heading and description content
- Add `actionsLayout` prop with "stacked" (default) and "inline" options for block variant
- Add `hideIcon` prop to hide the status icon
- Refactor block variant styles with new color system and layout improvements
- Reorganize Storybook stories with clearer naming (Block/* and Inline/*)
- Export AlertText and AlertTextProps from components package
2 changes: 2 additions & 0 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ const enhanceArgTypes = (context: any) => {
'appearance',
'position',
'placement',
'actionsLayout',
];

if (componentProps.includes(key)) {
Expand Down Expand Up @@ -326,6 +327,7 @@ const enhanceArgTypes = (context: any) => {
'appearance',
'position',
'placement',
'actionsLayout',
];

if (componentProps.includes(key)) {
Expand Down
62 changes: 38 additions & 24 deletions packages/components/src/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import type { VariantProps } from 'class-variance-authority';
import type { HTMLAttributes, Ref } from 'react';
import type { ComponentProps, HTMLAttributes, Ref } from 'react';

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

import { ButtonContext } from './Button';
import { ButtonGroupContext } from './ButtonGroup';
import { IconButton } from './IconButton';
import styles from './styles/Alert.module.css';

interface AlertTextProps extends ComponentProps<'div'> {
ref?: Ref<HTMLDivElement>;
}

/**
* AlertText wraps the title and description content within an Alert.
* Use this to group Heading and Text elements when using actionsLayout="inline".
*/
const AlertText = ({ className, ref, ...props }: AlertTextProps) => {
return <div ref={ref} className={`${styles.text} ${className ?? ''}`.trim()} {...props} />;
};

const alertStyles = cva(styles.base, {
variants: {
status: {
Expand All @@ -24,6 +35,10 @@ const alertStyles = cva(styles.base, {
default: styles.default,
inline: styles.inline,
},
actionsLayout: {
stacked: null,
inline: styles.actionsInline,
},
},
defaultVariants: {
status: 'neutral',
Expand All @@ -34,6 +49,8 @@ const alertStyles = cva(styles.base, {
interface AlertVariants extends VariantProps<typeof alertStyles> {}

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

const showIcon = !hideIcon && status !== 'neutral';
const resolvedActionsLayout = variant === 'default' ? actionsLayout : undefined;

return open ? (
<div ref={ref} {...props} role="alert" className={alertStyles({ status, variant, className })}>
{!hideIcon && status !== 'neutral' && (
<StatusIcon size="small" kind={status || 'info'} className={styles.icon} />
)}
<div
ref={ref}
{...props}
role="alert"
className={alertStyles({
status,
variant,
actionsLayout: resolvedActionsLayout,
className,
})}
>
{showIcon && <StatusIcon size="small" kind={status || 'info'} className={styles.icon} />}
<div className={styles.content}>
<Provider
values={[
[HeadingContext, { className: styles.heading }],
[
ButtonGroupContext,
{
className: styles.buttonGroup,
},
],
[
ButtonContext,
variant === 'inline'
? {
className: styles.inlineAction,
size: 'medium' as const,
variant: 'default' as const,
}
: {},
],
[ButtonGroupContext, { className: styles.buttonGroup }],
]}
>
{children}
Expand All @@ -103,5 +117,5 @@ const Alert = ({
) : null;
};

export { Alert, alertStyles };
export type { AlertProps };
export { Alert, AlertText, alertStyles };
export type { AlertProps, AlertTextProps };
4 changes: 3 additions & 1 deletion packages/components/src/ButtonGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ interface ButtonGroupProps extends GroupProps, VariantProps<typeof buttonGroupSt

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

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

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

ButtonGroup.displayName = 'ButtonGroup';

export { ButtonGroup, ButtonGroupContext, buttonGroupStyles };
export type { ButtonGroupProps };
4 changes: 2 additions & 2 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import './styles/base.css';
import './styles/themes.css';

export type { AlertProps } from './Alert';
export type { AlertProps, AlertTextProps } from './Alert';
export type { AutocompleteProps } from './Autocomplete';
export type { AvatarProps, InitialsAvatarProps } from './Avatar';
export type { BreadcrumbProps, BreadcrumbsProps } from './Breadcrumbs';
Expand Down Expand Up @@ -94,7 +94,7 @@ export type {
TreeProps,
} from './Tree';

export { Alert, alertStyles } from './Alert';
export { Alert, AlertText, alertStyles } from './Alert';
export { Autocomplete } from './Autocomplete';
export { Avatar, avatarStyles, InitialsAvatar } from './Avatar';
export {
Expand Down
121 changes: 79 additions & 42 deletions packages/components/src/styles/Alert.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@
--alert-color-bg-warning: #3c170c;
}

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

/* Status icon styles */
&.error .icon {
fill: var(--lp-color-fill-feedback-error);
}
Expand All @@ -48,14 +50,16 @@
}
}

.default,
.inline {
padding: var(--lp-spacing-500);
/* Base styles for the block variant */
.default {
/* 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. */
padding: var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-500);
background-color: var(--alert-color-bg-neutral);
border: 1px solid var(--alert-color-border-neutral);
position: relative;
min-height: var(--lp-size-48);

/* Status borders and background colors */
&.error {
border-color: var(--alert-color-border-error);
background-color: var(--alert-color-bg-error);
Expand All @@ -76,52 +80,76 @@
background-color: var(--alert-color-bg-warning);
}

&:has(.heading) {
/* biome-ignore lint/style/noDescendingSpecificity: ignore */
& .icon {
transform: translateY(var(--lp-size-2));
}
& .icon {
transform: translateY(var(--lp-size-10));
}

/* Let's keep the close button in the default position, but position relative in case we want to move it later. */
& .close {
position: relative;
/* 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. */
.content {
padding: var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-300) 0;
}
}

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

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

/* Add extra right padding when there's a close button to prevent text from overlapping the button */
&:has(.close) .content {
padding-right: var(--lp-size-48);
}
}

/* Inline actions variant */
.actionsInline {
/* Switch the direction of the content to row and make sure it fills its container. */
& .content {
flex-direction: row;
align-items: center;
gap: var(--lp-spacing-400);
align-items: flex-start;
flex: 1;
gap: var(--lp-spacing-500);

/* Unset padding on content to prevent it looking too big if there are actions present. */
padding: unset;
}

/* 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. */
&:has(.inlineAction),
&:has(.close) {
padding: 7px var(--lp-spacing-300) 7px var(--lp-spacing-500);
/* 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. */
& .text {
padding: calc(var(--lp-spacing-300) - var(--lp-size-2)) 0;
}

&:has(.inlineAction) {
.close {
margin-left: 0;
}
/* Need a specific selector to override the padding on the content container when there is a close button. */
&:has(.close) .content {
padding-right: unset;
}

/* Adjust icon position to compensate for the different padding */
& .icon {
transform: translateY(var(--lp-spacing-300));
}

/* Move the button group into position. We don't need a top margin here, and we want to flush it to the right */
& .buttonGroup {
margin-top: unset;
margin-left: auto;
}
}

.inlineAction {
flex-shrink: 0;
margin-left: auto;
/* Remove the absolute positioning from the close button so that it can participate in the flex layout. */
& .close {
position: unset;
top: unset;
right: unset;
}
}

/* Base styles for the content container */
.content {
min-width: 0;
flex: 1;
Expand All @@ -131,21 +159,30 @@
align-items: flex-start;
}

.content .heading {
font: var(--lp-text-body-2-semibold);
}

.buttonGroup {
margin-top: var(--lp-spacing-500);
/* Base styles for the text container */
.text {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}

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

/* Sometimes we want to make text bold, but we need to make sure this maps to the correct font weight. */
/* Sometimes we want to make text bold, but we need to make sure this maps to semibold, not bold. */
.content strong,
.content b {
font-weight: var(--lp-font-weight-semibold);
}

/* Inline variant styles */
.inline {
align-items: center;

& .close {
margin-left: var(--lp-spacing-300);
}
}
Comment thread
cmwinters marked this conversation as resolved.
Loading
Loading