Skip to content

Commit ea67c26

Browse files
committed
feat: add alert component
1 parent 26c4ecc commit ea67c26

5 files changed

Lines changed: 197 additions & 0 deletions

File tree

examples/nextjs-app/src/modules/pages/primitives-demo.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import {
1111
Menu,
1212
Table,
1313
Tabs,
14+
Alert,
15+
AlertTitle,
16+
AlertDescription,
1417
} from "@tailor-platform/app-shell";
1518
import {
1619
LayoutDashboard as LayoutDashboardIcon,
@@ -505,6 +508,31 @@ export const primitiveComponentsDemoResource = defineResource({
505508
</Table.Root>
506509
</Card.Content>
507510
</Card.Root>
511+
512+
{/* Alert */}
513+
<Card.Root>
514+
<Card.Header title="Alert" />
515+
<Card.Content>
516+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
517+
<Alert>
518+
<AlertTitle>Default Alert</AlertTitle>
519+
<AlertDescription>This is a default alert message.</AlertDescription>
520+
</Alert>
521+
<Alert variant="success">
522+
<AlertTitle>Success</AlertTitle>
523+
<AlertDescription>Operation completed successfully.</AlertDescription>
524+
</Alert>
525+
<Alert variant="error">
526+
<AlertTitle>Error</AlertTitle>
527+
<AlertDescription>Something went wrong. Please try again.</AlertDescription>
528+
</Alert>
529+
<Alert variant="neutral">
530+
<AlertTitle>Information</AlertTitle>
531+
<AlertDescription>This is a neutral informational message.</AlertDescription>
532+
</Alert>
533+
</div>
534+
</Card.Content>
535+
</Card.Root>
508536
</Layout.Column>
509537
</Layout>
510538
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`Alert > snapshots > default variant 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-primary/10 astw:text-primary astw:border-primary/20 *:data-[slot=alert-description]:astw:text-primary/80"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-alert" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><line x1="12" x2="12" y1="8" y2="12"></line><line x1="12" x2="12.01" y1="16" y2="16"></line></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Title</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">Description</div></div>"`;
4+
5+
exports[`Alert > snapshots > error variant 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-destructive/10 astw:text-destructive astw:border-destructive/20 *:data-[slot=alert-description]:astw:text-destructive/80"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="m15 9-6 6"></path><path d="m9 9 6 6"></path></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Error</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">Something went wrong</div></div>"`;
6+
7+
exports[`Alert > snapshots > neutral variant 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-secondary astw:text-secondary-foreground astw:border-border *:data-[slot=alert-description]:astw:text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-info" aria-hidden="true"><circle cx="12" cy="12" r="10"></circle><path d="M12 16v-4"></path><path d="M12 8h.01"></path></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Info</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">Note this</div></div>"`;
8+
9+
exports[`Alert > snapshots > success variant 1`] = `"<div data-slot="alert" role="alert" class="astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&amp;>svg]:size-[17.5px] astw:[&amp;>svg]:self-center astw:[&amp;>svg]:shrink-0 astw:[&amp;>svg]:text-current astw:bg-green-500/10 astw:text-green-700 astw:border-green-500/20 dark:astw:text-green-400 *:data-[slot=alert-description]:astw:text-green-700/80 dark:*:data-[slot=alert-description]:astw:text-green-400/80"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big" aria-hidden="true"><path d="M21.801 10A10 10 0 1 1 17 3.335"></path><path d="m9 11 3 3L22 4"></path></svg><div data-slot="alert-title" class="astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight">Success</div><div data-slot="alert-description" class="astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&amp;_p]:astw:leading-relaxed">Operation completed</div></div>"`;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { afterEach, describe, expect, it } from "vitest";
2+
import { cleanup, render, screen } from "@testing-library/react";
3+
import { Alert, AlertTitle, AlertDescription } from "./alert";
4+
5+
afterEach(() => {
6+
cleanup();
7+
});
8+
9+
describe("Alert", () => {
10+
describe("snapshots", () => {
11+
it("default variant", () => {
12+
const { container } = render(
13+
<Alert>
14+
<AlertTitle>Title</AlertTitle>
15+
<AlertDescription>Description</AlertDescription>
16+
</Alert>,
17+
);
18+
expect(container.innerHTML).toMatchSnapshot();
19+
});
20+
21+
it("success variant", () => {
22+
const { container } = render(
23+
<Alert variant="success">
24+
<AlertTitle>Success</AlertTitle>
25+
<AlertDescription>Operation completed</AlertDescription>
26+
</Alert>,
27+
);
28+
expect(container.innerHTML).toMatchSnapshot();
29+
});
30+
31+
it("error variant", () => {
32+
const { container } = render(
33+
<Alert variant="error">
34+
<AlertTitle>Error</AlertTitle>
35+
<AlertDescription>Something went wrong</AlertDescription>
36+
</Alert>,
37+
);
38+
expect(container.innerHTML).toMatchSnapshot();
39+
});
40+
41+
it("neutral variant", () => {
42+
const { container } = render(
43+
<Alert variant="neutral">
44+
<AlertTitle>Info</AlertTitle>
45+
<AlertDescription>Note this</AlertDescription>
46+
</Alert>,
47+
);
48+
expect(container.innerHTML).toMatchSnapshot();
49+
});
50+
});
51+
52+
it("renders with role=alert", () => {
53+
render(<Alert>Content</Alert>);
54+
expect(screen.getByRole("alert")).toBeDefined();
55+
});
56+
57+
it("renders title and description", () => {
58+
render(
59+
<Alert>
60+
<AlertTitle>Test Title</AlertTitle>
61+
<AlertDescription>Test Description</AlertDescription>
62+
</Alert>,
63+
);
64+
expect(screen.getByText("Test Title")).toBeDefined();
65+
expect(screen.getByText("Test Description")).toBeDefined();
66+
});
67+
68+
it("accepts custom className", () => {
69+
render(<Alert className="astw:mt-4">Content</Alert>);
70+
const alert = screen.getByRole("alert");
71+
expect(alert.className).toContain("astw:mt-4");
72+
});
73+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as React from "react";
2+
import { cva, type VariantProps } from "class-variance-authority";
3+
import { AlertCircleIcon, CheckCircleIcon, InfoIcon, XCircleIcon } from "lucide-react";
4+
import { cn } from "../lib/utils";
5+
6+
const alertVariants = cva(
7+
"astw:relative astw:w-full astw:rounded-lg astw:border astw:px-4 astw:py-3 astw:text-sm astw:grid astw:grid-cols-[calc(var(--spacing)*4.5)_1fr] astw:gap-x-3 astw:gap-y-0.5 astw:items-start astw:[&>svg]:size-[17.5px] astw:[&>svg]:self-center astw:[&>svg]:shrink-0 astw:[&>svg]:text-current",
8+
{
9+
variants: {
10+
variant: {
11+
default:
12+
"astw:bg-primary/10 astw:text-primary astw:border-primary/20 *:data-[slot=alert-description]:astw:text-primary/80",
13+
success:
14+
"astw:bg-green-500/10 astw:text-green-700 astw:border-green-500/20 dark:astw:text-green-400 *:data-[slot=alert-description]:astw:text-green-700/80 dark:*:data-[slot=alert-description]:astw:text-green-400/80",
15+
error:
16+
"astw:bg-destructive/10 astw:text-destructive astw:border-destructive/20 *:data-[slot=alert-description]:astw:text-destructive/80",
17+
neutral:
18+
"astw:bg-secondary astw:text-secondary-foreground astw:border-border *:data-[slot=alert-description]:astw:text-muted-foreground",
19+
},
20+
},
21+
defaultVariants: {
22+
variant: "default",
23+
},
24+
},
25+
);
26+
27+
const variantIcons: Record<
28+
NonNullable<VariantProps<typeof alertVariants>["variant"]>,
29+
React.ElementType
30+
> = {
31+
default: AlertCircleIcon,
32+
success: CheckCircleIcon,
33+
error: XCircleIcon,
34+
neutral: InfoIcon,
35+
};
36+
37+
type AlertProps = React.ComponentProps<"div"> & VariantProps<typeof alertVariants>;
38+
39+
function Alert({ className, variant = "default", children, ...props }: AlertProps) {
40+
const Icon = variantIcons[variant ?? "default"];
41+
return (
42+
<div
43+
data-slot="alert"
44+
role="alert"
45+
className={cn(alertVariants({ variant }), className)}
46+
{...props}
47+
>
48+
<Icon />
49+
{children}
50+
</div>
51+
);
52+
}
53+
54+
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
55+
return (
56+
<div
57+
data-slot="alert-title"
58+
className={cn(
59+
"astw:col-start-2 astw:line-clamp-1 astw:min-h-4 astw:font-medium astw:tracking-tight",
60+
className,
61+
)}
62+
{...props}
63+
/>
64+
);
65+
}
66+
67+
function AlertDescription({ className, ...props }: React.ComponentProps<"div">) {
68+
return (
69+
<div
70+
data-slot="alert-description"
71+
className={cn(
72+
"astw:col-start-2 astw:grid astw:justify-items-start astw:gap-1 astw:text-sm [&_p]:astw:leading-relaxed",
73+
className,
74+
)}
75+
{...props}
76+
/>
77+
);
78+
}
79+
80+
export { Alert, AlertTitle, AlertDescription, alertVariants, type AlertProps };

packages/core/src/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ export { useToast } from "./hooks/use-toast";
8383
export { useOverrideBreadcrumb } from "./hooks/use-override-breadcrumb";
8484

8585
// Components
86+
export {
87+
Alert,
88+
AlertTitle,
89+
AlertDescription,
90+
alertVariants,
91+
type AlertProps,
92+
} from "./components/alert";
8693
export { Badge, badgeVariants, type BadgeProps } from "./components/badge";
8794
export type { BadgeVariant, BadgeOptions } from "./components/badge-list";
8895
export { DescriptionCard, type DescriptionCardProps } from "./components/description-card";

0 commit comments

Comments
 (0)