From cbbe19b47d6edb93b012c91b5858eab9eb307b95 Mon Sep 17 00:00:00 2001 From: Adam Bull Date: Thu, 2 Oct 2025 14:01:17 +0100 Subject: [PATCH 1/2] feat: alert update --- src/components/Alert/index.stories.tsx | 171 ++++++++++++------ src/components/Alert/index.tsx | 232 +++++++++++++++++-------- src/components/Alert/types.ts | 2 +- 3 files changed, 276 insertions(+), 129 deletions(-) diff --git a/src/components/Alert/index.stories.tsx b/src/components/Alert/index.stories.tsx index aa072553..c81475ce 100644 --- a/src/components/Alert/index.stories.tsx +++ b/src/components/Alert/index.stories.tsx @@ -1,12 +1,12 @@ import { Alert } from '.' import { Meta, StoryObj } from '@storybook/react-vite' import { variants } from './types' -import { fn } from 'storybook/test' +import { Button } from '../Button' const defaultDecorators = [ // eslint-disable-next-line @typescript-eslint/no-explicit-any (Story: any) => ( -
{Story()}
+
{Story()}
), ] const meta: Meta = { @@ -18,6 +18,9 @@ const meta: Meta = { options: variants, }, }, + parameters: { + layout: 'fullscreen', + }, } export default meta @@ -27,7 +30,30 @@ type Story = StoryObj export const Default: Story = { args: { variant: 'default', - children: 'This is an alert', + children: [ + , + Default Alert, + This is a default alert message., + , + ], + }, + decorators: defaultDecorators, +} + +export const DefaultWithCTA: Story = { + args: { + variant: 'default', + children: [ + , + Default Alert, + This is a default alert message., + + + , + , + ], }, decorators: defaultDecorators, } @@ -35,7 +61,22 @@ export const Default: Story = { export const Success: Story = { args: { variant: 'success', - children: 'This is an alert', + children: [ + , + Success!, + + Your changes have been saved successfully. + , + + + + , + , + ], }, decorators: defaultDecorators, } @@ -43,7 +84,22 @@ export const Success: Story = { export const Error: Story = { args: { variant: 'error', - children: 'This is an alert', + children: [ + , + Error, + + Something went wrong. Please try again. + , + + + + , + , + ], }, decorators: defaultDecorators, } @@ -51,15 +107,20 @@ export const Error: Story = { export const Warning: Story = { args: { variant: 'warning', - children: 'This is an alert', - }, - decorators: defaultDecorators, -} - -export const Feature: Story = { - args: { - variant: 'feature', - children: 'This is an alert', + children: [ + , + Warning, + This action cannot be undone., + + + + , + , + ], }, decorators: defaultDecorators, } @@ -67,28 +128,22 @@ export const Feature: Story = { export const Info: Story = { args: { variant: 'info', - children: 'This is an alert', - }, - decorators: defaultDecorators, -} - -export const Inline: Story = { - args: { - variant: 'default', - inline: true, - children: 'This is an alert', - }, - decorators: defaultDecorators, -} - -export const Dismissible: Story = { - args: { - variant: 'default', - dismissible: true, - children: 'This is an alert', - onDismiss: fn().mockImplementation(() => { - console.log('dismissed') - }), + children: [ + , + Information, + + Here's some helpful information for you. + , + + + + , + , + ], }, decorators: defaultDecorators, } @@ -96,27 +151,37 @@ export const Dismissible: Story = { export const Multiline: Story = { args: { variant: 'error', - dismissible: true, - children: ( -
-

Whoops!

-

- We couldn't find the API you were looking for. -

-
- ), + children: [ + , + Whoops!, + + We couldn't find the API you were looking for. + , + + + + , + , + ], }, decorators: defaultDecorators, } -export const WithContainer: Story = { +export const Brand: Story = { args: { - variant: 'default', - useContainer: true, - children: 'This is an alert', + variant: 'brand', + children: [ + , + Brand Alert, + + This alert features our signature rainbow border. + , + , + ], }, - parameters: { - decorators: [], - }, - decorators: [(Story) =>
{Story()}
], + decorators: defaultDecorators, } diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx index 0d176371..9e705ca5 100644 --- a/src/components/Alert/index.tsx +++ b/src/components/Alert/index.tsx @@ -1,12 +1,10 @@ +import * as React from 'react' import { cva, type VariantProps } from 'class-variance-authority' import { Modifier, Variant } from './types' import { Icon } from '@/components/Icon' import { iconNames } from '../Icon/names' -import { useState } from 'react' import { cn } from '@/lib/utils' -const flexClasses = 'flex flex-row items-center gap-3' - const alertVariants = cva<{ variant: { [k in Variant]: string @@ -15,11 +13,13 @@ const alertVariants = cva<{ [k in Modifier]: string } }>( - `min-w-48 max-h-fit flex flex-row subpixel-antialiased font-light items-center px-3 pr-2 py-2 w-full border`, + cn( + 'relative flex flex-row items-center gap-3 p-4 w-full border rounded-xs text-sm tracking-[0.03em] subpixel-antialiased' + ), { variants: { variant: { - default: 'bg-card', + default: 'bg-card text-card-foreground border-neutral-softest', success: 'bg-success-softest text-default-success border-success-softest', error: @@ -27,92 +27,174 @@ const alertVariants = cva<{ warning: 'bg-warning-softest text-default-warning border-warning-softest', info: 'bg-information-softest text-default-information border-information-softest', - feature: 'bg-feature text-feature-foreground', + brand: + 'bg-transparent text-card-foreground border-transparent before:absolute before:content-[""] before:-z-10 before:pointer-events-none before:bg-[conic-gradient(from_220deg,hsl(334,54%,13%),hsl(4,67%,47%),hsl(23,96%,62%),hsl(68,52%,72%),hsl(108,24%,41%),hsl(154,100%,7%),hsl(220,100%,12%),hsl(214,69%,50%),hsl(216,100%,80%),hsl(334,54%,13%))] after:absolute after:content-[""] after:bg-card after:inset-[0px] after:-z-[5]', }, modifiers: { inline: 'inline-flex', }, }, + compoundVariants: [ + { + variant: 'brand', + className: + 'before:inset-[-1px] before:rounded-[calc(theme(borderRadius.xs))] after:rounded-[calc(theme(borderRadius.xs))]', + }, + ], } ) +// Alert subcomponents +const AlertIcon = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & { + name?: (typeof iconNames)[number] + size?: 'small' | 'medium' | 'large' + } +>(({ className, name, size = 'medium', ...props }, ref) => ( +
+ {name && } +
+)) +AlertIcon.displayName = 'Alert.Icon' + +const AlertTitle = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +AlertTitle.displayName = 'Alert.Title' + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +AlertDescription.displayName = 'Alert.Description' + +const AlertFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +AlertFooter.displayName = 'Alert.Footer' + +const AlertDismiss = React.forwardRef< + HTMLButtonElement, + React.ButtonHTMLAttributes & { + onDismiss?: () => void + } +>(({ className, onDismiss, ...props }, ref) => ( + +)) +AlertDismiss.displayName = 'Alert.Dismiss' + export type AlertProps = { - variant: NonNullable['variant']> + variant?: NonNullable['variant']> children: React.ReactNode inline?: boolean - dismissible?: boolean - onDismiss?: () => void - iconName?: (typeof iconNames)[number] useContainer?: boolean className?: string } -const iconForVariant: Record = - { - default: 'info', - success: 'check', - error: 'circle-alert', - warning: 'circle-alert', - info: 'info', - feature: 'star', - } +const BaseAlert = React.forwardRef( + ( + { variant = 'default', children, inline = false, className, ...props }, + ref + ) => { + const childArray = React.Children.toArray(children) -export function Alert({ - variant = 'default', - children, - inline = false, - dismissible = true, - onDismiss, - iconName, - useContainer = false, - className, -}: AlertProps) { - const [isDismissing, setIsDismissing] = useState(false) - const handleDismiss = () => { - setIsDismissing(true) - onDismiss?.() - } - const icon = iconName ?? iconForVariant[variant] - const innerContent = ( -
-
- {icon && } -
-
{children}
-
- ) + const iconChild = childArray.find( + (child) => + React.isValidElement(child) && + (child.type === AlertIcon || + (child.type as { displayName?: string })?.displayName === + 'Alert.Icon') + ) - const dismissableContent = dismissible && ( -
- -
- ) + const dismissChild = childArray.find( + (child) => + React.isValidElement(child) && + (child.type === AlertDismiss || + (child.type as { displayName?: string })?.displayName === + 'Alert.Dismiss') + ) + + const titleChild = childArray.find( + (child) => + React.isValidElement(child) && + (child.type === AlertTitle || + (child.type as { displayName?: string })?.displayName === + 'Alert.Title') + ) - return ( -
- {useContainer ? ( -
- {innerContent} - {dismissableContent} + const descriptionChild = childArray.find( + (child) => + React.isValidElement(child) && + (child.type === AlertDescription || + (child.type as { displayName?: string })?.displayName === + 'Alert.Description') + ) + + const footerChild = childArray.find( + (child) => + React.isValidElement(child) && + (child.type === AlertFooter || + (child.type as { displayName?: string })?.displayName === + 'Alert.Footer') + ) + + return ( +
+ {iconChild} +
+
+ {titleChild} + {descriptionChild} +
+ {footerChild}
- ) : ( - <> - {innerContent} - {dismissableContent} - - )} -
- ) -} + {dismissChild} +
+ ) + } +) + +// Create compound component +export const Alert = Object.assign(BaseAlert, { + Icon: AlertIcon, + Title: AlertTitle, + Description: AlertDescription, + Footer: AlertFooter, + Dismiss: AlertDismiss, +}) diff --git a/src/components/Alert/types.ts b/src/components/Alert/types.ts index 6e48fcc9..ebb6c796 100644 --- a/src/components/Alert/types.ts +++ b/src/components/Alert/types.ts @@ -4,7 +4,7 @@ export const variants = [ 'error', 'warning', 'info', - 'feature', + 'brand', ] as const export type Variant = (typeof variants)[number] From 79bdcb70db6dbbab630ed464f2a0147ad1e9beb6 Mon Sep 17 00:00:00 2001 From: Adam Bull Date: Thu, 2 Oct 2025 14:06:42 +0100 Subject: [PATCH 2/2] fix: stories --- src/components/Alert/index.stories.tsx | 2 +- src/components/Alert/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Alert/index.stories.tsx b/src/components/Alert/index.stories.tsx index c81475ce..209658db 100644 --- a/src/components/Alert/index.stories.tsx +++ b/src/components/Alert/index.stories.tsx @@ -6,7 +6,7 @@ import { Button } from '../Button' const defaultDecorators = [ // eslint-disable-next-line @typescript-eslint/no-explicit-any (Story: any) => ( -
{Story()}
+
{Story()}
), ] const meta: Meta = { diff --git a/src/components/Alert/index.tsx b/src/components/Alert/index.tsx index 9e705ca5..55b7a51c 100644 --- a/src/components/Alert/index.tsx +++ b/src/components/Alert/index.tsx @@ -57,7 +57,7 @@ const AlertIcon = React.forwardRef< className={cn('mt-2 size-6 flex-shrink-0 self-start', className)} {...props} > - {name && } + {name && }
)) AlertIcon.displayName = 'Alert.Icon'