From 6ea323b92fc74280602f641449021ab48daab33f Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Wed, 11 Mar 2026 21:45:32 +0800 Subject: [PATCH] feat: add reusable pattern components (PR2) Create src/components/patterns/ with 6 pure-presentation components that abstract repeated layout patterns from settings/bridge pages: - SettingsCard: rounded card with optional title/description - FieldRow: label+description left, control right, optional separator - SectionPage: max-width page container with consistent spacing - StatusBanner: success/warning/error/info inline status messages - EmptyState: centered empty state with icon, title, description, action - IconAction: ghost icon button with optional tooltip wrapper All components are zero-side-effect, only import from React and @/lib/utils (plus ui/button and ui/tooltip for IconAction). Co-Authored-By: Claude Opus 4.6 --- src/components/patterns/EmptyState.tsx | 23 ++++++++++++++ src/components/patterns/FieldRow.tsx | 28 +++++++++++++++++ src/components/patterns/IconAction.tsx | 40 ++++++++++++++++++++++++ src/components/patterns/SectionPage.tsx | 22 +++++++++++++ src/components/patterns/SettingsCard.tsx | 23 ++++++++++++++ src/components/patterns/StatusBanner.tsx | 29 +++++++++++++++++ src/components/patterns/index.ts | 6 ++++ 7 files changed, 171 insertions(+) create mode 100644 src/components/patterns/EmptyState.tsx create mode 100644 src/components/patterns/FieldRow.tsx create mode 100644 src/components/patterns/IconAction.tsx create mode 100644 src/components/patterns/SectionPage.tsx create mode 100644 src/components/patterns/SettingsCard.tsx create mode 100644 src/components/patterns/StatusBanner.tsx create mode 100644 src/components/patterns/index.ts diff --git a/src/components/patterns/EmptyState.tsx b/src/components/patterns/EmptyState.tsx new file mode 100644 index 0000000..4d9352d --- /dev/null +++ b/src/components/patterns/EmptyState.tsx @@ -0,0 +1,23 @@ +import { type ReactElement, type ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +interface EmptyStateProps { + icon?: ReactElement; + title: string; + description?: string; + action?: ReactNode; + className?: string; +} + +export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) { + return ( +
+ {icon &&
{icon}
} +

{title}

+ {description && ( +

{description}

+ )} + {action &&
{action}
} +
+ ); +} diff --git a/src/components/patterns/FieldRow.tsx b/src/components/patterns/FieldRow.tsx new file mode 100644 index 0000000..fee697a --- /dev/null +++ b/src/components/patterns/FieldRow.tsx @@ -0,0 +1,28 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +interface FieldRowProps { + label: string; + description?: string; + children: ReactNode; + separator?: boolean; + className?: string; +} + +export function FieldRow({ label, description, children, separator, className }: FieldRowProps) { + return ( +
+
+
{label}
+ {description && ( +
{description}
+ )} +
+
{children}
+
+ ); +} diff --git a/src/components/patterns/IconAction.tsx b/src/components/patterns/IconAction.tsx new file mode 100644 index 0000000..78b8c72 --- /dev/null +++ b/src/components/patterns/IconAction.tsx @@ -0,0 +1,40 @@ +import { type ReactElement, forwardRef, type ComponentPropsWithoutRef } from "react"; +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; + +interface IconActionProps extends Omit, "variant" | "size" | "children"> { + icon: ReactElement; + tooltip?: string; + size?: "sm" | "md"; +} + +export const IconAction = forwardRef( + function IconAction({ icon, tooltip, size = "md", className, ...props }, ref) { + const button = ( + + ); + + if (tooltip) { + return ( + + {button} + {tooltip} + + ); + } + + return button; + } +); diff --git a/src/components/patterns/SectionPage.tsx b/src/components/patterns/SectionPage.tsx new file mode 100644 index 0000000..b3b32a9 --- /dev/null +++ b/src/components/patterns/SectionPage.tsx @@ -0,0 +1,22 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +const maxWidthMap = { + sm: "max-w-xl", + md: "max-w-2xl", + lg: "max-w-3xl", +} as const; + +interface SectionPageProps { + children: ReactNode; + maxWidth?: "sm" | "md" | "lg"; + className?: string; +} + +export function SectionPage({ children, maxWidth = "lg", className }: SectionPageProps) { + return ( +
+ {children} +
+ ); +} diff --git a/src/components/patterns/SettingsCard.tsx b/src/components/patterns/SettingsCard.tsx new file mode 100644 index 0000000..e3cf1fb --- /dev/null +++ b/src/components/patterns/SettingsCard.tsx @@ -0,0 +1,23 @@ +import { type ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +interface SettingsCardProps { + title?: string; + description?: string; + children: ReactNode; + className?: string; +} + +export function SettingsCard({ title, description, children, className }: SettingsCardProps) { + return ( +
+ {(title || description) && ( +
+ {title &&

{title}

} + {description &&

{description}

} +
+ )} + {children} +
+ ); +} diff --git a/src/components/patterns/StatusBanner.tsx b/src/components/patterns/StatusBanner.tsx new file mode 100644 index 0000000..3550492 --- /dev/null +++ b/src/components/patterns/StatusBanner.tsx @@ -0,0 +1,29 @@ +import { type ReactElement, type ReactNode } from "react"; +import { cn } from "@/lib/utils"; + +const variantStyles = { + success: "bg-green-500/10 text-green-600 dark:text-green-400", + warning: "bg-orange-500/10 text-orange-600 dark:text-orange-400", + error: "bg-red-500/10 text-red-600 dark:text-red-400", + info: "bg-blue-500/10 text-blue-600 dark:text-blue-400", +} as const; + +interface StatusBannerProps { + variant: "success" | "warning" | "error" | "info"; + icon?: ReactElement; + children: ReactNode; + className?: string; +} + +export function StatusBanner({ variant, icon, children, className }: StatusBannerProps) { + return ( +
+ {icon &&
{icon}
} +
{children}
+
+ ); +} diff --git a/src/components/patterns/index.ts b/src/components/patterns/index.ts new file mode 100644 index 0000000..a1f31e2 --- /dev/null +++ b/src/components/patterns/index.ts @@ -0,0 +1,6 @@ +export { SettingsCard } from "./SettingsCard"; +export { FieldRow } from "./FieldRow"; +export { SectionPage } from "./SectionPage"; +export { StatusBanner } from "./StatusBanner"; +export { EmptyState } from "./EmptyState"; +export { IconAction } from "./IconAction";