From 33b6405695e68c2d9f7cd98bbf371d9f0c5aaa1d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 24 Oct 2025 19:48:28 +0530 Subject: [PATCH] [WEB-5160] chore: propel banner and archived work item improvements (#7999) --- .../(detail)/[archivedIssueId]/page.tsx | 45 +++-- packages/propel/package.json | 4 + packages/propel/src/banner/banner.stories.tsx | 181 ++++++++++++++++++ packages/propel/src/banner/banner.tsx | 131 +++++++++++++ packages/propel/src/banner/helper.tsx | 46 +++++ packages/propel/src/banner/index.ts | 3 + packages/propel/tsdown.config.ts | 1 + 7 files changed, 399 insertions(+), 12 deletions(-) create mode 100644 packages/propel/src/banner/banner.stories.tsx create mode 100644 packages/propel/src/banner/banner.tsx create mode 100644 packages/propel/src/banner/helper.tsx create mode 100644 packages/propel/src/banner/index.ts diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx index 550bfa7b8..45430c2bf 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx @@ -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(() => { ) : ( -
-
- {workspaceSlug && projectId && archivedIssueId && ( - - )} + <> + } + action={ + + } + className="border-b border-custom-border-200" + /> +
+
+ {workspaceSlug && projectId && archivedIssueId && ( + + )} +
-
+ )} ); diff --git a/packages/propel/package.json b/packages/propel/package.json index df630fcfc..46abc585a 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -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" diff --git a/packages/propel/src/banner/banner.stories.tsx b/packages/propel/src/banner/banner.stories.tsx new file mode 100644 index 000000000..70187155a --- /dev/null +++ b/packages/propel/src/banner/banner.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +// Sample icons for different variants +const SuccessIcon = () => ( + + + + +); + +const ErrorIcon = () => ( + + + + + +); + +const WarningIcon = () => ( + + + + + +); + +const InfoIcon = () => ( + + + + + +); + +const CloseButton = ({ onClick }: { onClick?: () => void }) => ( + +); + +// ============================================================================ +// Interactive Stories +// ============================================================================ + +export const Interactive: Story = { + args: { + variant: "info", + title: "This is an interactive banner. Use the controls to customize it.", + icon: , + dismissible: true, + }, +}; + +// ============================================================================ +// Main Variants +// ============================================================================ + +export const Success: Story = { + args: { + variant: "success", + title: "Operation completed successfully", + icon: , + action: , + }, +}; + +export const Error: Story = { + args: { + variant: "error", + title: "An error occurred while processing your request", + icon: , + action: , + }, +}; + +export const Warning: Story = { + args: { + variant: "warning", + title: "Your session will expire in 5 minutes", + icon: , + action: , + }, +}; + +export const Info: Story = { + args: { + variant: "info", + title: "New features are available. Check out what's new!", + icon: , + action: , + }, +}; diff --git a/packages/propel/src/banner/banner.tsx b/packages/propel/src/banner/banner.tsx new file mode 100644 index 000000000..f3c22ee22 --- /dev/null +++ b/packages/propel/src/banner/banner.tsx @@ -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, "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( + ( + { + 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
{icon}
; + } + return null; + }; + + // Render dismiss button if dismissible + const renderDismissButton = () => { + if (!dismissible) return null; + + return ( + + ); + }; + + return ( +
+ {/* Left side: Icon and Title */} +
+ {renderIcon()} + {title &&
{title}
} + {children} +
+ + {/* Right side: Actions */} + {(action || dismissible) && ( +
+ {action} + {renderDismissButton()} +
+ )} +
+ ); + } +); + +Banner.displayName = "Banner"; + +// Export variant types for external use +export type BannerVariant = TBannerVariant; diff --git a/packages/propel/src/banner/helper.tsx b/packages/propel/src/banner/helper.tsx new file mode 100644 index 000000000..cf128a209 --- /dev/null +++ b/packages/propel/src/banner/helper.tsx @@ -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"; diff --git a/packages/propel/src/banner/index.ts b/packages/propel/src/banner/index.ts new file mode 100644 index 000000000..12d0a82b4 --- /dev/null +++ b/packages/propel/src/banner/index.ts @@ -0,0 +1,3 @@ +export { Banner } from "./banner"; +export type { BannerProps, BannerVariant } from "./banner"; +export type { TBannerVariant } from "./helper"; diff --git a/packages/propel/tsdown.config.ts b/packages/propel/tsdown.config.ts index 677fea529..d698a2b90 100644 --- a/packages/propel/tsdown.config.ts +++ b/packages/propel/tsdown.config.ts @@ -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",