feat(ui): add state components and apply design system compliance
Add new reusable components: - EmptyState: for empty list/search states with icon, title, description - ErrorState: for error displays with optional retry button - LoadingState: for loading states with Spinner and message - PageHeader: for consistent page headers with title, description, actions Apply components across pages: - agents, collections, tool-search pages use new state components - stats page uses LoadingState and ErrorState - faq page uses Icon for chevrons - health page uses Table components - dashboard agent forms use Input, Select, Textarea, Label - Replace custom div spinners with Spinner component
This commit is contained in:
parent
499a38ce1b
commit
f0d271720c
15 changed files with 607 additions and 328 deletions
|
|
@ -218,6 +218,22 @@
|
|||
"./ToolCard/ToolCard": {
|
||||
"types": "./dist/ToolCard/ToolCard.d.ts",
|
||||
"default": "./dist/ToolCard/ToolCard.js"
|
||||
},
|
||||
"./EmptyState/EmptyState": {
|
||||
"types": "./dist/EmptyState/EmptyState.d.ts",
|
||||
"default": "./dist/EmptyState/EmptyState.js"
|
||||
},
|
||||
"./ErrorState/ErrorState": {
|
||||
"types": "./dist/ErrorState/ErrorState.d.ts",
|
||||
"default": "./dist/ErrorState/ErrorState.js"
|
||||
},
|
||||
"./LoadingState/LoadingState": {
|
||||
"types": "./dist/LoadingState/LoadingState.d.ts",
|
||||
"default": "./dist/LoadingState/LoadingState.js"
|
||||
},
|
||||
"./PageHeader/PageHeader": {
|
||||
"types": "./dist/PageHeader/PageHeader.d.ts",
|
||||
"default": "./dist/PageHeader/PageHeader.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
|
|
|||
110
packages/ui/src/EmptyState/EmptyState.tsx
Normal file
110
packages/ui/src/EmptyState/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { cn } from '@tpmjs/utils/cn';
|
||||
import { forwardRef } from 'react';
|
||||
import { Icon, type IconName } from '../Icon/Icon';
|
||||
|
||||
export interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Icon to display */
|
||||
icon: IconName;
|
||||
/** Title text */
|
||||
title: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Optional action element (button, link, etc.) */
|
||||
action?: React.ReactNode;
|
||||
/** Size variant */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const sizeConfig = {
|
||||
sm: {
|
||||
container: 'py-8',
|
||||
iconContainer: 'w-12 h-12',
|
||||
iconSize: 'md' as const,
|
||||
title: 'text-base',
|
||||
description: 'text-sm',
|
||||
},
|
||||
md: {
|
||||
container: 'py-12',
|
||||
iconContainer: 'w-16 h-16',
|
||||
iconSize: 'lg' as const,
|
||||
title: 'text-lg',
|
||||
description: 'text-sm',
|
||||
},
|
||||
lg: {
|
||||
container: 'py-16',
|
||||
iconContainer: 'w-20 h-20',
|
||||
iconSize: 'lg' as const,
|
||||
title: 'text-xl',
|
||||
description: 'text-base',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* EmptyState component
|
||||
*
|
||||
* A consistent empty state pattern for displaying when no data is available.
|
||||
* Use this for empty lists, search results with no matches, or first-time states.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { EmptyState } from '@tpmjs/ui/EmptyState/EmptyState';
|
||||
* import { Button } from '@tpmjs/ui/Button/Button';
|
||||
*
|
||||
* function MyComponent() {
|
||||
* return (
|
||||
* <EmptyState
|
||||
* icon="folder"
|
||||
* title="No collections found"
|
||||
* description="Create your first collection to get started."
|
||||
* action={<Button>Create Collection</Button>}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const EmptyState = forwardRef<HTMLDivElement, EmptyStateProps>(
|
||||
(
|
||||
{ className, icon, title, description, action, size = 'md', ...props },
|
||||
ref
|
||||
) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-center', config.container, className)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-4',
|
||||
config.iconContainer
|
||||
)}
|
||||
>
|
||||
<Icon icon={icon} size={config.iconSize} className="text-primary" />
|
||||
</div>
|
||||
<h3
|
||||
className={cn(
|
||||
'font-medium text-foreground mb-2',
|
||||
config.title
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
{description && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-secondary mb-4 max-w-md mx-auto',
|
||||
config.description
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
{action && <div className="flex justify-center">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EmptyState.displayName = 'EmptyState';
|
||||
98
packages/ui/src/ErrorState/ErrorState.tsx
Normal file
98
packages/ui/src/ErrorState/ErrorState.tsx
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { cn } from '@tpmjs/utils/cn';
|
||||
import { forwardRef } from 'react';
|
||||
import { Button } from '../Button/Button';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
export interface ErrorStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Optional title (defaults to "Error") */
|
||||
title?: string;
|
||||
/** Error message to display */
|
||||
message: string;
|
||||
/** Optional retry callback - shows retry button if provided */
|
||||
onRetry?: () => void;
|
||||
/** Size variant */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const sizeConfig = {
|
||||
sm: {
|
||||
container: 'py-8',
|
||||
iconSize: 'md' as const,
|
||||
title: 'text-base',
|
||||
message: 'text-sm',
|
||||
},
|
||||
md: {
|
||||
container: 'py-12',
|
||||
iconSize: 'lg' as const,
|
||||
title: 'text-lg',
|
||||
message: 'text-sm',
|
||||
},
|
||||
lg: {
|
||||
container: 'py-16',
|
||||
iconSize: 'lg' as const,
|
||||
title: 'text-xl',
|
||||
message: 'text-base',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* ErrorState component
|
||||
*
|
||||
* A consistent error state pattern for displaying error messages with optional retry.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { ErrorState } from '@tpmjs/ui/ErrorState/ErrorState';
|
||||
*
|
||||
* function MyComponent() {
|
||||
* return (
|
||||
* <ErrorState
|
||||
* message="Failed to load data"
|
||||
* onRetry={() => refetch()}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const ErrorState = forwardRef<HTMLDivElement, ErrorStateProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
title = 'Error',
|
||||
message,
|
||||
onRetry,
|
||||
size = 'md',
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-center', config.container, className)}
|
||||
{...props}
|
||||
>
|
||||
<Icon
|
||||
icon="alertCircle"
|
||||
size={config.iconSize}
|
||||
className="mx-auto text-error mb-4"
|
||||
/>
|
||||
<h2 className={cn('font-medium text-foreground mb-2', config.title)}>
|
||||
{title}
|
||||
</h2>
|
||||
<p className={cn('text-foreground-secondary mb-4', config.message)}>
|
||||
{message}
|
||||
</p>
|
||||
{onRetry && (
|
||||
<Button onClick={onRetry} variant="default">
|
||||
Try Again
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ErrorState.displayName = 'ErrorState';
|
||||
81
packages/ui/src/LoadingState/LoadingState.tsx
Normal file
81
packages/ui/src/LoadingState/LoadingState.tsx
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { cn } from '@tpmjs/utils/cn';
|
||||
import { forwardRef } from 'react';
|
||||
import { Spinner } from '../Spinner/Spinner';
|
||||
|
||||
export interface LoadingStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Optional loading message */
|
||||
message?: string;
|
||||
/** Size variant */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const sizeConfig = {
|
||||
sm: {
|
||||
container: 'py-8',
|
||||
spinnerSize: 'sm' as const,
|
||||
messageClass: 'text-xs',
|
||||
gap: 'gap-2',
|
||||
},
|
||||
md: {
|
||||
container: 'py-16',
|
||||
spinnerSize: 'md' as const,
|
||||
messageClass: 'text-sm',
|
||||
gap: 'gap-3',
|
||||
},
|
||||
lg: {
|
||||
container: 'py-24',
|
||||
spinnerSize: 'lg' as const,
|
||||
messageClass: 'text-sm',
|
||||
gap: 'gap-4',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* LoadingState component
|
||||
*
|
||||
* A consistent loading state pattern with spinner and optional message.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { LoadingState } from '@tpmjs/ui/LoadingState/LoadingState';
|
||||
*
|
||||
* function MyComponent() {
|
||||
* if (isLoading) {
|
||||
* return <LoadingState message="Loading tools..." />;
|
||||
* }
|
||||
* return <div>Content</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const LoadingState = forwardRef<HTMLDivElement, LoadingStateProps>(
|
||||
({ className, message, size = 'md', ...props }, ref) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center justify-center',
|
||||
config.container,
|
||||
config.gap,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Spinner size={config.spinnerSize} />
|
||||
{message && (
|
||||
<span
|
||||
className={cn(
|
||||
'text-foreground-secondary font-mono tracking-wide',
|
||||
config.messageClass
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LoadingState.displayName = 'LoadingState';
|
||||
86
packages/ui/src/PageHeader/PageHeader.tsx
Normal file
86
packages/ui/src/PageHeader/PageHeader.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { cn } from '@tpmjs/utils/cn';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/** Page title */
|
||||
title: string;
|
||||
/** Optional description text */
|
||||
description?: string;
|
||||
/** Optional action elements (buttons, links, etc.) */
|
||||
actions?: React.ReactNode;
|
||||
/** Size variant */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const sizeConfig = {
|
||||
sm: {
|
||||
container: 'mb-4 space-y-1',
|
||||
title: 'text-xl sm:text-2xl',
|
||||
description: 'text-sm',
|
||||
},
|
||||
md: {
|
||||
container: 'mb-6 space-y-2',
|
||||
title: 'text-2xl sm:text-3xl',
|
||||
description: 'text-base',
|
||||
},
|
||||
lg: {
|
||||
container: 'mb-8 space-y-4',
|
||||
title: 'text-2xl sm:text-3xl md:text-4xl',
|
||||
description: 'text-lg',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* PageHeader component
|
||||
*
|
||||
* A consistent page header pattern with title, description, and optional actions.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { PageHeader } from '@tpmjs/ui/PageHeader/PageHeader';
|
||||
* import { Button } from '@tpmjs/ui/Button/Button';
|
||||
*
|
||||
* function MyPage() {
|
||||
* return (
|
||||
* <PageHeader
|
||||
* title="Tool Registry"
|
||||
* description="Browse and discover AI agent tools"
|
||||
* actions={<Button>Add Tool</Button>}
|
||||
* />
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const PageHeader = forwardRef<HTMLDivElement, PageHeaderProps>(
|
||||
(
|
||||
{ className, title, description, actions, size = 'md', ...props },
|
||||
ref
|
||||
) => {
|
||||
const config = sizeConfig[size];
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(config.container, className)} {...props}>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<h1 className={cn('font-bold text-foreground', config.title)}>
|
||||
{title}
|
||||
</h1>
|
||||
{description && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-foreground-secondary',
|
||||
config.description
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{actions && <div className="flex items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
PageHeader.displayName = 'PageHeader';
|
||||
|
|
@ -83,7 +83,7 @@ const TableRow = forwardRef<HTMLTableRowElement, TableRowProps>(
|
|||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'border-b border-border transition-colors',
|
||||
'border-b border-border bg-white dark:bg-zinc-900 transition-colors',
|
||||
interactive && 'cursor-pointer hover:bg-surface',
|
||||
selected && 'bg-surface',
|
||||
className
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue