base
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Banner } from "./banner";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Banner",
|
||||
component: Banner,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "select",
|
||||
options: ["success", "error", "warning", "info"],
|
||||
description: "Visual variant of the banner",
|
||||
},
|
||||
title: {
|
||||
control: "text",
|
||||
description: "Banner message text",
|
||||
},
|
||||
icon: {
|
||||
control: false,
|
||||
description: "Icon element to display before the title",
|
||||
},
|
||||
action: {
|
||||
control: false,
|
||||
description: "Action element(s) to display on the right side",
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Banner>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Sample icons for different variants
|
||||
function SuccessIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-success-primary"
|
||||
>
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||
<polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-danger-primary"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="15" y1="9" x2="9" y2="15" />
|
||||
<line x1="9" y1="9" x2="15" y2="15" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function WarningIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-yellow-600"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
||||
<line x1="12" y1="9" x2="12" y2="13" />
|
||||
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-blue-600"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
function CloseButton({ onClick }: { onClick?: () => void }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="rounded-sm p-1 transition-colors hover:bg-black/5 dark:hover:bg-white/5"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="text-text-secondary"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Interactive Stories
|
||||
// ============================================================================
|
||||
|
||||
export const Interactive: Story = {
|
||||
args: {
|
||||
variant: "info",
|
||||
title: "This is an interactive banner. Use the controls to customize it.",
|
||||
icon: <InfoIcon />,
|
||||
dismissible: true,
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Main Variants
|
||||
// ============================================================================
|
||||
|
||||
export const Success: Story = {
|
||||
args: {
|
||||
variant: "success",
|
||||
title: "Operation completed successfully",
|
||||
icon: <SuccessIcon />,
|
||||
action: <CloseButton />,
|
||||
},
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
variant: "error",
|
||||
title: "An error occurred while processing your request",
|
||||
icon: <ErrorIcon />,
|
||||
action: <CloseButton />,
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
variant: "warning",
|
||||
title: "Your session will expire in 5 minutes",
|
||||
icon: <WarningIcon />,
|
||||
action: <CloseButton />,
|
||||
},
|
||||
};
|
||||
|
||||
export const Info: Story = {
|
||||
args: {
|
||||
variant: "info",
|
||||
title: "New features are available. Check out what's new!",
|
||||
icon: <InfoIcon />,
|
||||
action: <CloseButton />,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { cn } from "../utils";
|
||||
import type { TBannerVariant } from "./helper";
|
||||
import {
|
||||
getBannerStyling,
|
||||
getBannerTitleStyling,
|
||||
getBannerActionStyling,
|
||||
getBannerDismissStyling,
|
||||
getBannerDismissIconStyling,
|
||||
} from "./helper";
|
||||
|
||||
export interface BannerProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
||||
/** Visual variant of the banner */
|
||||
variant?: TBannerVariant;
|
||||
/** Icon to display before the title */
|
||||
icon?: React.ReactNode;
|
||||
/** Banner title/message */
|
||||
title?: React.ReactNode;
|
||||
/** Action elements to display on the right side */
|
||||
action?: React.ReactNode;
|
||||
/** Whether the banner can be dismissed */
|
||||
dismissible?: boolean;
|
||||
/** Callback when banner is dismissed */
|
||||
onDismiss?: () => void;
|
||||
/** Whether to show the banner */
|
||||
visible?: boolean;
|
||||
/** Animation duration for show/hide */
|
||||
animationDuration?: number;
|
||||
}
|
||||
|
||||
export const Banner = React.forwardRef(function Banner(
|
||||
{
|
||||
icon,
|
||||
title,
|
||||
action,
|
||||
variant = "info",
|
||||
dismissible = false,
|
||||
onDismiss,
|
||||
visible = true,
|
||||
animationDuration = 200,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: BannerProps,
|
||||
ref: React.ForwardedRef<HTMLDivElement>
|
||||
) {
|
||||
// Handle dismissal
|
||||
const handleDismiss = () => {
|
||||
if (onDismiss) {
|
||||
onDismiss();
|
||||
}
|
||||
};
|
||||
|
||||
// Don't render if not visible
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get styling using helper functions
|
||||
const containerStyling = getBannerStyling(variant);
|
||||
const iconStyling = "flex items-center justify-center flex-shrink-0 size-5";
|
||||
const titleStyling = getBannerTitleStyling();
|
||||
const actionStyling = getBannerActionStyling();
|
||||
const dismissStyling = getBannerDismissStyling();
|
||||
const dismissIconStyling = getBannerDismissIconStyling();
|
||||
|
||||
// Render custom icon component if provided
|
||||
const renderIcon = () => {
|
||||
if (icon) {
|
||||
return <div className={cn(iconStyling)}>{icon}</div>;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Render dismiss button if dismissible
|
||||
const renderDismissButton = () => {
|
||||
if (!dismissible) return null;
|
||||
|
||||
return (
|
||||
<button onClick={handleDismiss} className={cn(dismissStyling)} aria-label="Dismiss banner">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={cn(dismissIconStyling)}
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(containerStyling, className)}
|
||||
style={{
|
||||
transitionDuration: `${animationDuration}ms`,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{/* Left side: Icon and Title */}
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
{renderIcon()}
|
||||
{title && <div className={cn(titleStyling)}>{title}</div>}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Right side: Actions */}
|
||||
{(action || dismissible) && (
|
||||
<div className={cn(actionStyling)}>
|
||||
{action}
|
||||
{renderDismissButton()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Banner.displayName = "Banner";
|
||||
|
||||
// Export variant types for external use
|
||||
export type BannerVariant = TBannerVariant;
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export type TBannerVariant = "success" | "error" | "warning" | "info";
|
||||
|
||||
export interface IBannerStyling {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export const bannerSizeStyling = {
|
||||
container: "py-3 px-6 h-12",
|
||||
icon: "w-5 h-5",
|
||||
title: "text-13",
|
||||
action: "gap-2",
|
||||
};
|
||||
|
||||
// TODO: update this with new color once its implemented
|
||||
// Banner variant styling
|
||||
export const bannerStyling: IBannerStyling = {
|
||||
success: "bg-success-subtle",
|
||||
error: "bg-danger-subtle",
|
||||
warning: "bg-yellow-500/10",
|
||||
info: "bg-blue-500/10",
|
||||
};
|
||||
|
||||
// Base banner styles
|
||||
export const bannerBaseStyles = "flex items-center justify-between w-full transition-all duration-200";
|
||||
|
||||
// Get banner container styling
|
||||
export const getBannerStyling = (variant: TBannerVariant): string => {
|
||||
const variantStyles = bannerStyling[variant];
|
||||
const sizeStyles = bannerSizeStyling.container;
|
||||
|
||||
return `${bannerBaseStyles} ${variantStyles} ${sizeStyles}`;
|
||||
};
|
||||
|
||||
// Get title styling
|
||||
export const getBannerTitleStyling = (): string =>
|
||||
`font-medium text-secondary flex-1 min-w-0 ${bannerSizeStyling.title}`;
|
||||
|
||||
// Get action container styling
|
||||
export const getBannerActionStyling = (): string => `flex items-center flex-shrink-0 ${bannerSizeStyling.action}`;
|
||||
|
||||
// Get dismiss button styling
|
||||
export const getBannerDismissStyling = (): string =>
|
||||
"rounded-sm p-1 hover:bg-surface-2 transition-colors flex-shrink-0";
|
||||
|
||||
// Get dismiss icon styling
|
||||
export const getBannerDismissIconStyling = (): string => "text-secondary";
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
export { Banner } from "./banner";
|
||||
export type { BannerProps, BannerVariant } from "./banner";
|
||||
export type { TBannerVariant } from "./helper";
|
||||
Reference in New Issue
Block a user