Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
185 changes: 101 additions & 84 deletions src/components/Alert/Alert.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { useState } from 'react';
import {
AlertCircle,
AlertTriangle,
CheckCircle,
Info as InfoIcon,
Bell,
ShieldAlert,
Lightbulb,
Zap,
} from 'lucide-react';
import { Alert, AlertTitle, AlertDescription } from './Alert';

// Icon registry for Storybook controls
const iconRegistry: Record<string, React.ReactElement> = {
none: undefined as unknown as React.ReactElement,
Comment on lines +15 to +17
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This story file references the React namespace in a type (React.ReactElement) but only imports useState from react. Unless the project has global React types enabled (uncommon), this will fail typecheck. Import the React types (e.g., import type { ReactElement } from 'react' or import type * as React from 'react') and use that in the iconRegistry type.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +17
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

iconRegistry uses none: undefined as unknown as React.ReactElement, which defeats the stricter icon typing and relies on a cast that can hide real mistakes. Prefer making the registry value type explicitly nullable/optional (e.g., Record<string, ReactElement | null> with none: null) so the Storybook mapping and Alert prop types align without unsafe assertions.

Suggested change
const iconRegistry: Record<string, React.ReactElement> = {
none: undefined as unknown as React.ReactElement,
const iconRegistry: Record<string, React.ReactElement | null> = {
none: null,

Copilot uses AI. Check for mistakes.
info: <InfoIcon size={16} />,
alertCircle: <AlertCircle size={16} />,
alertTriangle: <AlertTriangle size={16} />,
checkCircle: <CheckCircle size={16} />,
bell: <Bell size={16} />,
shieldAlert: <ShieldAlert size={16} />,
lightbulb: <Lightbulb size={16} />,
zap: <Zap size={16} />,
};

const meta: Meta<typeof Alert> = {
title: 'Components/Alert',
component: Alert,
Expand All @@ -17,6 +40,12 @@ const meta: Meta<typeof Alert> = {
dismissible: {
control: 'boolean',
},
icon: {
control: 'select',
options: Object.keys(iconRegistry),
mapping: iconRegistry,
description: 'Icon to display in the alert',
},
},
decorators: [
(Story) => (
Expand All @@ -31,84 +60,88 @@ export default meta;
type Story = StoryObj<typeof meta>;

export const Default: Story = {
render: () => (
<Alert>
<AlertTitle>Default Alert</AlertTitle>
<AlertDescription>This is a default alert message.</AlertDescription>
</Alert>
),
args: {
variant: 'default',
children: (
<>
<AlertTitle>Default Alert</AlertTitle>
<AlertDescription>This is a default alert message.</AlertDescription>
</>
),
},
};

export const Info: Story = {
render: () => (
<Alert variant="info">
<AlertTitle>Information</AlertTitle>
<AlertDescription>This is an informational message.</AlertDescription>
</Alert>
),
args: {
variant: 'info',
icon: <InfoIcon size={16} />,
children: (
<>
<AlertTitle>Information</AlertTitle>
<AlertDescription>This is an informational message.</AlertDescription>
</>
),
},
};

export const Success: Story = {
render: () => (
<Alert variant="success">
<AlertTitle>Success!</AlertTitle>
<AlertDescription>
Your changes have been saved successfully.
</AlertDescription>
</Alert>
),
args: {
variant: 'success',
icon: <CheckCircle size={16} />,
children: (
<>
<AlertTitle>Success!</AlertTitle>
<AlertDescription>
Your changes have been saved successfully.
</AlertDescription>
</>
),
},
};

export const Warning: Story = {
render: () => (
<Alert variant="warning">
<AlertTitle>Warning</AlertTitle>
<AlertDescription>
Your session is about to expire in 5 minutes.
</AlertDescription>
</Alert>
),
args: {
variant: 'warning',
icon: <AlertTriangle size={16} />,
children: (
<>
<AlertTitle>Warning</AlertTitle>
<AlertDescription>
Your session is about to expire in 5 minutes.
</AlertDescription>
</>
),
},
};

export const Danger: Story = {
render: () => (
<Alert variant="danger">
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</Alert>
),
args: {
variant: 'danger',
icon: <AlertCircle size={16} />,
children: (
<>
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Something went wrong. Please try again.
</AlertDescription>
</>
),
},
};

export const WithIcon: Story = {
render: () => (
<Alert
variant="info"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
}
>
<AlertTitle>Did you know?</AlertTitle>
<AlertDescription>
You can customize the look and feel of alerts with different variants.
</AlertDescription>
</Alert>
),
args: {
variant: 'info',
icon: <InfoIcon size={16} />,
children: (
<>
<AlertTitle>Did you know?</AlertTitle>
<AlertDescription>
You can customize the look and feel of alerts with different variants.
</AlertDescription>
</>
),
},
};

export const AllVariants: Story = {
Expand All @@ -118,19 +151,19 @@ export const AllVariants: Story = {
<AlertTitle>Default</AlertTitle>
<AlertDescription>Default alert style.</AlertDescription>
</Alert>
<Alert variant="info">
<Alert variant="info" icon={<InfoIcon size={16} />}>
<AlertTitle>Info</AlertTitle>
<AlertDescription>Informational alert style.</AlertDescription>
</Alert>
<Alert variant="success">
<Alert variant="success" icon={<CheckCircle size={16} />}>
<AlertTitle>Success</AlertTitle>
<AlertDescription>Success alert style.</AlertDescription>
</Alert>
<Alert variant="warning">
<Alert variant="warning" icon={<AlertTriangle size={16} />}>
<AlertTitle>Warning</AlertTitle>
<AlertDescription>Warning alert style.</AlertDescription>
</Alert>
<Alert variant="danger">
<Alert variant="danger" icon={<AlertCircle size={16} />}>
<AlertTitle>Danger</AlertTitle>
<AlertDescription>Danger alert style.</AlertDescription>
</Alert>
Expand All @@ -157,23 +190,7 @@ function DismissibleExample() {
variant="info"
dismissible
onDismiss={() => setVisible(false)}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
}
icon={<InfoIcon size={16} />}
>
<AlertTitle>Dismissible Alert</AlertTitle>
<AlertDescription>
Expand Down
23 changes: 5 additions & 18 deletions src/components/Alert/Alert.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { X } from 'lucide-react';
import { cn } from '../../utils/cn';

const alertVariants = cva(
[
'relative w-full rounded-lg border p-4',
'[&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-current',
'[&>svg+div]:translate-y-[-3px]',
'[&:has(svg)]:pl-11',
],
{
variants: {
Expand All @@ -33,7 +33,7 @@ export interface AlertProps
React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof alertVariants> {
/** Icon to display in the alert */
icon?: React.ReactNode;
icon?: React.ReactElement | null;
/** Whether the alert can be dismissed */
dismissible?: boolean;
/** Callback when the alert is dismissed */
Expand Down Expand Up @@ -73,12 +73,13 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
role="alert"
className={cn(
alertVariants({ variant }),
React.isValidElement(icon) && 'pl-11',
dismissible && 'pr-10',
className
)}
{...props}
>
{icon}
{React.isValidElement(icon) && icon}
<div>{children}</div>
{dismissible && (
<button
Expand All @@ -92,21 +93,7 @@ const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
)}
aria-label={dismissLabel}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
<X size={16} aria-hidden="true" />
</button>
)}
</div>
Expand Down
Loading