Compare commits

...
Author SHA1 Message Date
abeatrix 95208575fb feat(ui): add Banner component and rename default story to Overview
- Add new Banner component for simplified alert-style notifications
- Refactor Alert component to extract shared base styles
- Move AlertTitle and AlertDescription definitions for better organization
- Add default icons per variant (InfoIcon, AlertTriangleIcon, ClineLogoWhite)
- Rename "Default" story export to "Overview" for consistency
- Include Banner examples in alert stories
2025-12-15 12:57:30 -08:00
13 changed files with 151 additions and 67 deletions
+14 -6
View File
@@ -1,6 +1,7 @@
import type { Meta } from "@storybook/react-vite"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
import { BellIcon } from "lucide-react"
import { Alert, AlertDescription } from "./alert"
import { Banner } from "./banner"
const meta: Meta<typeof Alert> = {
title: "Ui/Alert",
@@ -17,22 +18,22 @@ const meta: Meta<typeof Alert> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-4 w-full max-w-md px-4">
<Alert title="Default" variant="default">
<AlertDescription>This is a default alert message.</AlertDescription>
</Alert>
<Alert isDismissible={false} title="Default Non Dismissible" variant="default">
<AlertDescription>This alert cannot be dismissed.</AlertDescription>
</Alert>
<Alert title="Warning" variant="warning">
<AlertDescription>This is a warning alert.</AlertDescription>
</Alert>
<Alert title="Error" variant="danger">
<AlertDescription>An error has occurred.</AlertDescription>
</Alert>
<Alert isDismissible={false} title="Non Dismissible" variant="default">
<AlertDescription>This alert cannot be dismissed.</AlertDescription>
</Alert>
<Alert icon={<ClineLogoWhite className="size-2" />} title="Brand" variant="cline">
<Alert title="Cline" variant="cline">
<AlertDescription>
<p className="my-1">How can I help you?</p>
<ul className="list-inside list-disc text-sm">
@@ -42,6 +43,13 @@ export const Default = () => (
</ul>
</AlertDescription>
</Alert>
<Banner description="This is an example description" icon={BellIcon} title="Banner" />
<Banner
description="This is an example description"
icon={BellIcon}
isDismissible={false}
title="Non Dismissable Banner"
/>
</div>
</div>
)
+68 -52
View File
@@ -1,63 +1,26 @@
import { cva, type VariantProps } from "class-variance-authority"
import { AlertTriangleIcon, XIcon } from "lucide-react"
import { AlertTriangleIcon, InfoIcon, XIcon } from "lucide-react"
import * as React from "react"
import ClineLogoWhite from "@/assets/ClineLogoWhite"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-sm border p-2 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:text-foreground [&>svg~*]:pl-7 flex flex-col gap-1 mb-1",
{
variants: {
variant: {
default: "bg-banner-background text-banner-foreground border-foreground/20",
warning: "bg-warning/50 border-foreground/20 [&>svg]:text-warning-foreground",
danger: "bg-input-error-background text-foreground border-foreground/20 [&>svg]:text-input-error-foreground",
cline: "bg-cline/20 text-fg-cline border-cline [&>svg]:text-fg-cline",
},
},
defaultVariants: {
variant: "default",
const BASE_CLASS =
"relative w-full rounded-sm border p-2 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:text-foreground [&>svg~*]:pl-7 flex flex-col gap-1 mb-1"
const alertVariants = cva(BASE_CLASS, {
variants: {
variant: {
default: "bg-banner-background text-banner-foreground border-foreground/20",
warning: "bg-warning/50 border-foreground/20 [&>svg]:text-warning-foreground",
danger: "bg-input-error-background text-foreground border-foreground/20 [&>svg]:text-input-error-foreground",
cline: "bg-cline/20 text-fg-cline border-cline [&>svg]:text-fg-cline",
},
},
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> &
VariantProps<typeof alertVariants> & { isDismissible?: boolean; title?: string; icon?: React.ReactNode }
>(({ className, variant, children, isDismissible = true, title, icon, ...props }, ref) => {
const [dismissed, setDismissed] = React.useState(false)
if (dismissed) {
return null
}
return (
<div className={cn(alertVariants({ variant }), className)} ref={ref} role="alert" {...props}>
<div className="flex items-center justify-between w-full">
<AlertTitle className="flex gap-1 w-full">
<span className="mr-1 shrink-0">{icon ?? <AlertTriangleIcon className="size-2" />}</span>
{title}
</AlertTitle>
{isDismissible && (
<Button
aria-label="Dismiss"
className="opacity-100 hover:opacity-100 justify-center"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setDismissed(true)
}}
size="icon"
variant="icon">
<XIcon />
</Button>
)}
</div>
{children}
</div>
)
defaultVariants: {
variant: "default",
},
})
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<HTMLHeadingElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
@@ -75,4 +38,57 @@ const AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttrib
)
AlertDescription.displayName = "AlertDescription"
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> &
VariantProps<typeof alertVariants> & {
isDismissible?: boolean
title?: string
icon?: React.ReactNode
description?: string
}
>(({ className, variant, children, isDismissible = true, title, icon, description, ...props }, ref) => {
const [dismissed, setDismissed] = React.useState(false)
if (dismissed) {
return null
}
return (
<div className={cn(alertVariants({ variant }), className)} ref={ref} role="alert" {...props}>
<div className="flex items-center justify-between w-full">
<AlertTitle className="flex gap-1 w-full">
<span className="mr-1 shrink-0 [&>svg]:size-2">
{icon ??
(variant === "cline" ? (
<ClineLogoWhite />
) : variant === "danger" ? (
<AlertTriangleIcon />
) : (
<InfoIcon />
))}
</span>
{title}
</AlertTitle>
{isDismissible && (
<Button
aria-label="Dismiss"
className="opacity-100 hover:opacity-100 justify-center"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setDismissed(true)
}}
size="icon"
variant="icon">
<XIcon />
</Button>
)}
</div>
{description && <AlertDescription>{description}</AlertDescription>}
{children}
</div>
)
})
Alert.displayName = "Alert"
export { Alert, AlertTitle, AlertDescription }
@@ -17,7 +17,7 @@ const meta: Meta<typeof Badge> = {
export default meta
export const Default = () => {
export const Overview = () => {
const variants = ["default", "info", "danger", "outline", "cline", "success", "warning"] as const
const types = [
@@ -0,0 +1,32 @@
import type { Meta } from "@storybook/react-vite"
import { BellIcon } from "lucide-react"
import { Banner } from "./banner"
const meta: Meta<typeof Banner> = {
title: "Ui/Banner",
component: Banner,
parameters: {
docs: {
description: {
component:
"Displays message with preset styles and configurable options for title and description. Includes optional dismiss functionality.",
},
},
},
}
export default meta
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-4 w-full max-w-md px-4">
<Banner description="This is an example description" icon={BellIcon} title="Banner" />
<Banner
description="This is an example description"
icon={BellIcon}
isDismissible={false}
title="Non Dismissable Banner"
/>
</div>
</div>
)
+28
View File
@@ -0,0 +1,28 @@
import { LucideProps } from "lucide-react"
import React from "react"
import { Alert } from "./alert"
const Banner = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
isDismissible?: boolean
title: string
icon: React.ForwardRefExoticComponent<Omit<LucideProps, "ref"> & React.RefAttributes<SVGSVGElement>>
description: string
}
>(({ className, children, isDismissible = true, title, icon, description, ...props }, ref) => {
return (
<Alert
className={className}
description={description}
icon={React.createElement(icon)}
isDismissible={isDismissible}
ref={ref}
title={title}
{...props}
/>
)
})
Banner.displayName = "Banner"
export { Banner }
@@ -17,7 +17,7 @@ const meta: Meta<typeof HoverCard> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center min-h-[400px]">
<div className="flex flex-col gap-8 w-full max-w-md px-4">
<HoverCard>
@@ -17,7 +17,7 @@ const meta: Meta<typeof Input> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-4 w-full max-w-md px-4">
<Input placeholder="Default text input" type="text" />
@@ -30,7 +30,7 @@ const meta: Meta<typeof Item> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-5 w-full max-w-md px-4">
<ItemGroup>
@@ -17,7 +17,7 @@ const meta: Meta<typeof Popover> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center min-h-[400px]">
<div className="flex flex-col gap-8 w-full max-w-md px-4">
<Popover>
@@ -16,7 +16,7 @@ const meta: Meta<typeof Progress> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-6 w-full max-w-md px-4">
<div className="space-y-2">
@@ -16,7 +16,7 @@ const meta: Meta<typeof Separator> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-8 w-full max-w-md px-4">
<div className="space-y-4">
@@ -16,7 +16,7 @@ const meta: Meta<typeof Switch> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center">
<div className="flex flex-col gap-6 w-full max-w-md px-4">
<div className="flex items-center justify-between">
@@ -18,7 +18,7 @@ const meta: Meta<typeof Tooltip> = {
export default meta
export const Default = () => (
export const Overview = () => (
<div className="w-screen flex justify-center items-center min-h-[400px]">
<div className="flex flex-col gap-8 w-full max-w-md px-4 items-center">
<Tooltip>