[WEB-5160] chore: propel banner and archived work item improvements (#7999)

This commit is contained in:
Anmol Singh Bhatia
2025-10-24 19:48:28 +05:30
committed by GitHub
parent 68fd2463f4
commit 33b6405695
7 changed files with 399 additions and 12 deletions

View File

@@ -1,9 +1,12 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import useSWR from "swr";
// ui
import { Banner } from "@plane/propel/banner";
import { Button } from "@plane/propel/button";
import { ArchiveIcon } from "@plane/propel/icons";
import { Loader } from "@plane/ui";
// components
import { PageHead } from "@/components/core/page-title";
@@ -16,6 +19,7 @@ import { useProject } from "@/hooks/store/use-project";
const ArchivedIssueDetailsPage = observer(() => {
// router
const { workspaceSlug, projectId, archivedIssueId } = useParams();
const router = useRouter();
// states
// hooks
const {
@@ -62,18 +66,35 @@ const ArchivedIssueDetailsPage = observer(() => {
</div>
</Loader>
) : (
<div className="flex h-full overflow-hidden">
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto">
{workspaceSlug && projectId && archivedIssueId && (
<IssueDetailRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={archivedIssueId.toString()}
is_archived
/>
)}
<>
<Banner
variant="warning"
title="This work item has been archived. Visit the Archives section to restore it."
icon={<ArchiveIcon className="size-4" />}
action={
<Button
variant="neutral-primary"
size="sm"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/archives/issues/`)}
>
Go to archives
</Button>
}
className="border-b border-custom-border-200"
/>
<div className="flex h-full overflow-hidden">
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto">
{workspaceSlug && projectId && archivedIssueId && (
<IssueDetailRoot
workspaceSlug={workspaceSlug.toString()}
projectId={projectId.toString()}
issueId={archivedIssueId.toString()}
is_archived
/>
)}
</div>
</div>
</div>
</>
)}
</>
);

View File

@@ -28,6 +28,10 @@
"import": "./dist/avatar/index.mjs",
"require": "./dist/avatar/index.js"
},
"./banner": {
"import": "./dist/banner/index.mjs",
"require": "./dist/banner/index.js"
},
"./button": {
"import": "./dist/button/index.mjs",
"require": "./dist/button/index.js"

View File

@@ -0,0 +1,181 @@
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
const SuccessIcon = () => (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-green-600"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
);
const ErrorIcon = () => (
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-red-600"
>
<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>
);
const WarningIcon = () => (
<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>
);
const InfoIcon = () => (
<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>
);
const CloseButton = ({ onClick }: { onClick?: () => void }) => (
<button
onClick={onClick}
className="rounded p-1 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
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 />,
},
};

View File

@@ -0,0 +1,131 @@
import React from "react";
import { cn } from "../utils";
import {
TBannerVariant,
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<HTMLDivElement, BannerProps>(
(
{
icon,
title,
action,
variant = "info",
dismissible = false,
onDismiss,
visible = true,
animationDuration = 200,
className,
children,
...props
},
ref
) => {
// 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 items-center gap-3 flex-1 min-w-0">
{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;

View File

@@ -0,0 +1,46 @@
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-sm",
action: "gap-2",
};
// TODO: update this with new color once its implemented
// Banner variant styling
export const bannerStyling: IBannerStyling = {
success: "bg-green-500/10",
error: "bg-red-500/10",
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-custom-text-200 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 p-1 hover:bg-custom-background-90 transition-colors flex-shrink-0";
// Get dismiss icon styling
export const getBannerDismissIconStyling = (): string => "text-custom-text-200";

View File

@@ -0,0 +1,3 @@
export { Banner } from "./banner";
export type { BannerProps, BannerVariant } from "./banner";
export type { TBannerVariant } from "./helper";

View File

@@ -5,6 +5,7 @@ export default defineConfig({
"src/accordion/index.ts",
"src/animated-counter/index.ts",
"src/avatar/index.ts",
"src/banner/index.ts",
"src/button/index.ts",
"src/calendar/index.ts",
"src/card/index.ts",