Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
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
85 changes: 66 additions & 19 deletions packages/components/src/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,57 @@
import type { VariantProps } from 'class-variance-authority';
import type { HTMLAttributes, Ref } from 'react';
import type { HTMLAttributes, ReactNode, Ref } from 'react';

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

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

const isButtonGroup = (child: ReactNode): boolean => {
if (!isValidElement(child)) return false;
const type = child.type as { displayName?: string; name?: string };
return (
child.type === ButtonGroup || type.displayName === 'ButtonGroup' || type.name === 'ButtonGroup'
);
};
Comment thread
cmwinters marked this conversation as resolved.
Outdated

const flattenChildren = (children: ReactNode): ReactNode[] => {
const result: ReactNode[] = [];

Children.forEach(children, (child) => {
if (isValidElement<{ children?: ReactNode }>(child) && child.type === Fragment) {
result.push(...flattenChildren(child.props.children));
} else {
result.push(child);
}
});

return result;
};

const separateChildren = (
children: ReactNode,
): { textContent: ReactNode[]; actions: ReactNode } => {
const textContent: ReactNode[] = [];
let actions: ReactNode = null;

const flatChildren = flattenChildren(children);

for (const child of flatChildren) {
if (isButtonGroup(child)) {
actions = child;
} else {
textContent.push(child);
}
}

return { textContent, actions };
};

const alertStyles = cva(styles.base, {
variants: {
status: {
Expand All @@ -24,17 +65,24 @@ const alertStyles = cva(styles.base, {
default: styles.default,
inline: styles.inline,
},
actionsLayout: {
stacked: styles.actionsStacked,
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
inline: styles.actionsInline,
},
},
defaultVariants: {
status: 'neutral',
variant: 'default',
actionsLayout: 'stacked',
},
});

interface AlertVariants extends VariantProps<typeof alertStyles> {}

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

const showIcon = status !== 'neutral' && !(hideIcon && variant === 'default');
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
const resolvedActionsLayout = variant === 'default' ? actionsLayout : undefined;
const { textContent, actions } = separateChildren(children);

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={[
Expand All @@ -74,19 +130,10 @@ const Alert = ({
className: styles.buttonGroup,
},
],
[
ButtonContext,
variant === 'inline'
? {
className: styles.inlineAction,
size: 'medium' as const,
variant: 'default' as const,
}
: {},
],
]}
>
{children}
<div className={styles.text}>{textContent}</div>
{actions}
</Provider>
</div>
{isDismissable && (
Expand Down
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 };
80 changes: 40 additions & 40 deletions packages/components/src/styles/Alert.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
}
}

.default,
.inline {
/* Stacked actions variant */
.default {
padding: var(--lp-spacing-500);
background-color: var(--alert-color-bg-neutral);
border: 1px solid var(--alert-color-border-neutral);
Expand All @@ -76,50 +76,49 @@
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-2));
}

/* Let's keep the close button in the default position, but position relative in case we want to move it later. */
& .close {
.close {
position: relative;
bottom: var(--lp-spacing-300);
left: var(--lp-spacing-300);
}
}

.inline {
align-items: center;
gap: var(--lp-spacing-300);
padding: var(--lp-spacing-400) var(--lp-spacing-500);

& .close {
margin-left: var(--lp-spacing-300);
.buttonGroup {
margin-top: var(--lp-spacing-500);
}
}

/* Inline actions variant */
.actionsInline {
padding: var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-300) var(--lp-spacing-500);

& .content {
flex-direction: row;
align-items: center;
gap: var(--lp-spacing-400);
align-items: flex-start;
gap: var(--lp-spacing-500);
}

/* 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);
& .icon {
transform: translateY(var(--lp-spacing-300));
}

&:has(.inlineAction) {
.close {
margin-left: 0;
}
/* This magic number is used to ensure the text baseline is aligned with the trailing actions and dismiss buttons */
& .text {
padding: calc(var(--lp-spacing-300) - 2px) 0;
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
}

& .buttonGroup {
margin-top: unset;
margin-left: auto;
}
}

.inlineAction {
flex-shrink: 0;
margin-left: auto;
& .close {
bottom: unset;
left: unset;
}
}

.content {
Expand All @@ -131,21 +130,22 @@
align-items: flex-start;
}

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

.buttonGroup {
margin-top: var(--lp-spacing-500);
}

.default .close {
bottom: var(--lp-spacing-300);
left: var(--lp-spacing-300);
}

/* Sometimes we want to make text bold, but we need to make sure this maps to the correct font weight. */
.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