|
| 1 | +import type { ReactNode } from "react"; |
| 2 | + |
| 3 | +type CalloutType = "info" | "warning" | "tip" | "danger" | "note"; |
| 4 | + |
| 5 | +interface CalloutProps { |
| 6 | + type?: CalloutType; |
| 7 | + title?: string; |
| 8 | + children?: ReactNode; |
| 9 | +} |
| 10 | + |
| 11 | +const styles: Record<CalloutType, { container: string; iconBg: string; icon: string; title: string }> = { |
| 12 | + info: { |
| 13 | + container: "bg-blue-50/70 dark:bg-blue-500/10 border-l-blue-500", |
| 14 | + iconBg: "bg-blue-100 dark:bg-blue-500/20", |
| 15 | + icon: "text-blue-600 dark:text-blue-400", |
| 16 | + title: "text-blue-900 dark:text-blue-200", |
| 17 | + }, |
| 18 | + note: { |
| 19 | + container: "bg-gray-50/70 dark:bg-gray-500/10 border-l-gray-400", |
| 20 | + iconBg: "bg-gray-100 dark:bg-gray-500/20", |
| 21 | + icon: "text-gray-600 dark:text-gray-400", |
| 22 | + title: "text-gray-900 dark:text-gray-200", |
| 23 | + }, |
| 24 | + warning: { |
| 25 | + container: "bg-orange-50/70 dark:bg-orange-500/10 border-l-orange-500", |
| 26 | + iconBg: "bg-orange-100 dark:bg-orange-500/20", |
| 27 | + icon: "text-orange-600 dark:text-orange-400", |
| 28 | + title: "text-orange-900 dark:text-orange-200", |
| 29 | + }, |
| 30 | + tip: { |
| 31 | + container: "bg-green-50/70 dark:bg-green-500/10 border-l-green-500", |
| 32 | + iconBg: "bg-green-100 dark:bg-green-500/20", |
| 33 | + icon: "text-green-600 dark:text-green-400", |
| 34 | + title: "text-green-900 dark:text-green-200", |
| 35 | + }, |
| 36 | + danger: { |
| 37 | + container: "bg-red-50/70 dark:bg-red-500/10 border-l-red-500", |
| 38 | + iconBg: "bg-red-100 dark:bg-red-500/20", |
| 39 | + icon: "text-red-600 dark:text-red-400", |
| 40 | + title: "text-red-900 dark:text-red-200", |
| 41 | + }, |
| 42 | +}; |
| 43 | + |
| 44 | +const icons: Record<CalloutType, ReactNode> = { |
| 45 | + info: ( |
| 46 | + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 47 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| 48 | + </svg> |
| 49 | + ), |
| 50 | + note: ( |
| 51 | + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 52 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> |
| 53 | + </svg> |
| 54 | + ), |
| 55 | + warning: ( |
| 56 | + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 57 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> |
| 58 | + </svg> |
| 59 | + ), |
| 60 | + tip: ( |
| 61 | + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 62 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /> |
| 63 | + </svg> |
| 64 | + ), |
| 65 | + danger: ( |
| 66 | + <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 67 | + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> |
| 68 | + </svg> |
| 69 | + ), |
| 70 | +}; |
| 71 | + |
| 72 | +const defaultTitles: Record<CalloutType, string> = { |
| 73 | + info: "Info", |
| 74 | + note: "Note", |
| 75 | + warning: "Warning", |
| 76 | + tip: "Tip", |
| 77 | + danger: "Danger", |
| 78 | +}; |
| 79 | + |
| 80 | +export function Callout({ type = "info", title, children }: CalloutProps) { |
| 81 | + const style = styles[type]; |
| 82 | + const icon = icons[type]; |
| 83 | + const displayTitle = title || defaultTitles[type]; |
| 84 | + |
| 85 | + return ( |
| 86 | + <div className={`not-prose my-5 rounded-lg border-l-4 overflow-hidden ${style.container}`}> |
| 87 | + <div className="px-4 py-3.5"> |
| 88 | + <div className="flex items-start gap-3"> |
| 89 | + <div className={`shrink-0 flex items-center justify-center w-6 h-6 rounded-md ${style.iconBg}`}> |
| 90 | + <div className={style.icon}>{icon}</div> |
| 91 | + </div> |
| 92 | + <div className="flex-1 min-w-0"> |
| 93 | + {displayTitle && ( |
| 94 | + <p className={`font-semibold text-[13px] mb-1 ${style.title}`}>{displayTitle}</p> |
| 95 | + )} |
| 96 | + <div className="text-[13px] text-zinc-700 dark:text-zinc-300 leading-relaxed [&>p]:m-0 [&>p+p]:mt-1.5"> |
| 97 | + {children} |
| 98 | + </div> |
| 99 | + </div> |
| 100 | + </div> |
| 101 | + </div> |
| 102 | + </div> |
| 103 | + ); |
| 104 | +} |
0 commit comments