index.tsx107 lines · main
1import type { LucideIcon } from 'lucide-react'
2import { SquarePlus } from 'lucide-react'
3import { createElement, isValidElement, ReactNode } from 'react'
4import { cn } from 'ui'
5
6export 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 */
52export 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}