empty-state.tsx62 lines · main
1'use client';
2
3import { motion } from 'motion/react';
4
5/**
6 * Friendly, spacious empty/error placeholder — replaces the old cramped
7 * one-liner. Give it an icon, a plain-words message, and (for error
8 * states) an action like a retry button.
9 */
10export function EmptyState({
11 icon,
12 title,
13 message,
14 action,
15}: {
16 icon?: React.ReactNode;
17 title: string;
18 message?: string;
19 action?: React.ReactNode;
20}) {
21 return (
22 <motion.div
23 initial={{ opacity: 0, y: 8 }}
24 animate={{ opacity: 1, y: 0 }}
25 transition={{ duration: 0.4, ease: 'easeOut' }}
26 className="flex flex-col items-center justify-center gap-4 rounded-xl border border-dashed border-[var(--color-border-subtle)] bg-[var(--color-surface)] px-8 py-16 text-center"
27 >
28 {icon ? <span className="text-[var(--color-text-muted)]">{icon}</span> : null}
29 <div className="flex flex-col gap-1.5">
30 <p className="font-mono text-sm text-[var(--color-text)]">{title}</p>
31 {message ? (
32 <p className="mx-auto max-w-md font-mono text-xs leading-relaxed text-[var(--color-text-subtle)]">
33 {message}
34 </p>
35 ) : null}
36 </div>
37 {action ? <div className="mt-1">{action}</div> : null}
38 </motion.div>
39 );
40}
41
42/** House-styled action button for EmptyState (e.g. "retry"). */
43export function EmptyStateButton({
44 onClick,
45 children,
46 disabled,
47}: {
48 onClick: () => void;
49 children: React.ReactNode;
50 disabled?: boolean;
51}) {
52 return (
53 <button
54 type="button"
55 onClick={onClick}
56 disabled={disabled}
57 className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-[var(--color-surface-raised)] px-4 py-2 font-mono text-xs text-[var(--color-text)] transition hover:border-[var(--color-border-strong)] hover:text-[var(--color-primary)] disabled:opacity-50"
58 >
59 {children}
60 </button>
61 );
62}