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 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b8c7cba53
commit
6ea323b92f
7 files changed
+171
No files matched your search
@@ -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 (
|
||||
<div className={cn("flex flex-col items-center justify-center py-12 text-center", className)}>
|
||||
{icon && <div className="mb-4 text-muted-foreground">{icon}</div>}
|
||||
<h3 className="text-sm font-medium">{title}</h3>
|
||||
{description && (
|
||||
<p className="mt-1 text-xs text-muted-foreground max-w-sm">{description}</p>
|
||||
)}
|
||||
{action && <div className="mt-4">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn(
|
||||
"flex items-center justify-between gap-4",
|
||||
separator && "border-t border-border/30 pt-4",
|
||||
className
|
||||
)}>
|
||||
<div className="space-y-0.5 flex-1 min-w-0">
|
||||
<div className="text-sm font-medium">{label}</div>
|
||||
{description && (
|
||||
<div className="text-xs text-muted-foreground">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-shrink-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<ComponentPropsWithoutRef<typeof Button>, "variant" | "size" | "children"> {
|
||||
icon: ReactElement;
|
||||
tooltip?: string;
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
export const IconAction = forwardRef<HTMLButtonElement, IconActionProps>(
|
||||
function IconAction({ icon, tooltip, size = "md", className, ...props }, ref) {
|
||||
const button = (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
size === "sm" ? "h-7 w-7" : "h-8 w-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
);
|
||||
@@ -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 (
|
||||
<div className={cn("mx-auto w-full space-y-6", maxWidthMap[maxWidth], className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn("rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm", className)}>
|
||||
{(title || description) && (
|
||||
<div className="space-y-1">
|
||||
{title && <h3 className="text-sm font-medium">{title}</h3>}
|
||||
{description && <p className="text-xs text-muted-foreground">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn(
|
||||
"flex items-center gap-2 rounded-md px-3 py-2 text-xs",
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}>
|
||||
{icon && <div className="flex-shrink-0">{icon}</div>}
|
||||
<div className="flex-1 min-w-0">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
Reference in new issue
Block a user