index.tsx107 lines · main
| 1 | import type { LucideIcon } from 'lucide-react' |
| 2 | import { SquarePlus } from 'lucide-react' |
| 3 | import { createElement, isValidElement, ReactNode } from 'react' |
| 4 | import { cn } from 'ui' |
| 5 | |
| 6 | export interface EmptyStatePresentationalProps { |
| 7 | icon?: LucideIcon | ReactNode |
| 8 | title: string |
| 9 | description?: string | ReactNode |
| 10 | children?: ReactNode |
| 11 | className?: string |
| 12 | iconSize?: number |
| 13 | iconClassName?: string |
| 14 | contentClassName?: string |
| 15 | } |
| 16 | |
| 17 | /** |
| 18 | * EmptyStatePresentational component for displaying presentational empty states with icons, titles, descriptions, and optional actions. |
| 19 | * |
| 20 | * This component is specifically designed for initial state scenarios where users are learning about a feature for the first time. |
| 21 | * It emphasizes value propositions and provides clear actions users can take. |
| 22 | * |
| 23 | * Supports both Lucide icons (component types) and custom ReactNode icons (like forwardRef components from 'icons' package). |
| 24 | * Automatically handles rendering icons whether they're passed as component types or pre-rendered elements. |
| 25 | * |
| 26 | * @example |
| 27 | * ```tsx |
| 28 | * // With Lucide icon component type |
| 29 | * <EmptyStatePresentational |
| 30 | * icon={DatabaseBackup} |
| 31 | * title="No backups yet" |
| 32 | * description="Check again tomorrow." |
| 33 | * /> |
| 34 | * |
| 35 | * // With custom icon element |
| 36 | * <EmptyStatePresentational |
| 37 | * icon={<Loader2 className="animate-spin" />} |
| 38 | * title="Loading..." |
| 39 | * description="Please wait" |
| 40 | * /> |
| 41 | * |
| 42 | * // With action button |
| 43 | * <EmptyStatePresentational |
| 44 | * icon={BucketPlus} |
| 45 | * title="Create a vector bucket" |
| 46 | * description="Store, index, and query your vector embeddings at scale." |
| 47 | * > |
| 48 | * <Button>Create bucket</Button> |
| 49 | * </EmptyStatePresentational> |
| 50 | * ``` |
| 51 | */ |
| 52 | export const EmptyStatePresentational = ({ |
| 53 | icon: Icon, |
| 54 | title, |
| 55 | description, |
| 56 | children, |
| 57 | className, |
| 58 | iconSize = 24, |
| 59 | iconClassName, |
| 60 | contentClassName, |
| 61 | }: EmptyStatePresentationalProps) => { |
| 62 | // Use SquarePlus as default icon if none is provided |
| 63 | const iconToRender = Icon || SquarePlus |
| 64 | |
| 65 | // Extract content rendering to avoid duplication between icon and no-icon cases |
| 66 | const textContent = ( |
| 67 | <div className={cn('flex flex-col items-center text-center text-balance', contentClassName)}> |
| 68 | <h3>{title}</h3> |
| 69 | {description && <p className="text-foreground-light text-sm max-w-[640px]">{description}</p>} |
| 70 | </div> |
| 71 | ) |
| 72 | |
| 73 | return ( |
| 74 | <aside |
| 75 | className={cn( |
| 76 | 'border border-dashed w-full bg-surface-100 rounded-lg px-4 py-10 flex flex-col items-center gap-y-3', |
| 77 | className |
| 78 | )} |
| 79 | > |
| 80 | <div className="flex flex-col gap-y-3 items-center"> |
| 81 | {/* |
| 82 | Handle different icon types: |
| 83 | 1. If it's already a React element (pre-rendered), render it directly |
| 84 | 2. If it's a function (LucideIcon component type) or forwardRef component (has $$typeof), |
| 85 | instantiate it with createElement and apply default props |
| 86 | 3. Otherwise, render as-is (fallback for other ReactNode types) |
| 87 | */} |
| 88 | {isValidElement(iconToRender) |
| 89 | ? iconToRender |
| 90 | : typeof iconToRender === 'function' || |
| 91 | (typeof iconToRender === 'object' && |
| 92 | iconToRender !== null && |
| 93 | '$$typeof' in iconToRender) |
| 94 | ? createElement(iconToRender as LucideIcon, { |
| 95 | size: iconSize, |
| 96 | strokeWidth: 1.5, |
| 97 | className: cn('text-foreground-muted', iconClassName), |
| 98 | }) |
| 99 | : iconToRender} |
| 100 | {textContent} |
| 101 | </div> |
| 102 | |
| 103 | {/* Optional children (typically action buttons) */} |
| 104 | {children} |
| 105 | </aside> |
| 106 | ) |
| 107 | } |