|
1 | | -import { ReactNode } from 'react'; |
2 | | - |
3 | | -type InfoPanelType = |
4 | | - | 'info' |
5 | | - | 'warning' |
6 | | - | 'danger' |
7 | | - | 'success' |
8 | | - | 'note' |
9 | | - | 'tip' |
10 | | - | 'neutral' |
11 | | - | 'quote'; |
12 | | - |
13 | | -type InfoPanelProps = { |
| 1 | +import React, { ReactNode } from 'react'; |
| 2 | +import { |
| 3 | + Info, |
| 4 | + AlertTriangle, |
| 5 | + ShieldAlert, |
| 6 | + BadgeCheck, |
| 7 | + AlignLeft , |
| 8 | + Sparkles, |
| 9 | + CheckCircle, |
| 10 | + CircleEllipsis, |
| 11 | +} from 'lucide-react'; |
| 12 | + |
| 13 | +export type InfoPanelType = |
| 14 | + | 'info' |
| 15 | + | 'warning' |
| 16 | + | 'danger' |
| 17 | + | 'success' |
| 18 | + | 'note' |
| 19 | + | 'tip' |
| 20 | + | 'neutral' |
| 21 | + | 'quote'; |
| 22 | + |
| 23 | +interface InfoPanelProps { |
14 | 24 | type?: InfoPanelType; |
15 | 25 | children: ReactNode; |
| 26 | +} |
| 27 | + |
| 28 | +const typeStyles: Record< |
| 29 | + InfoPanelType, |
| 30 | + { |
| 31 | + bg: string; |
| 32 | + border: string; |
| 33 | + text: string; |
| 34 | + icon: JSX.Element; |
| 35 | + iconColor: string; |
| 36 | + titleColor: string; |
| 37 | + } |
| 38 | +> = { |
| 39 | + info: { |
| 40 | + bg: 'bg-blue-50 dark:bg-blue-900/30', |
| 41 | + border: 'border-l-4 border-blue-400', |
| 42 | + text: 'text-blue-900 dark:text-blue-100', |
| 43 | + icon: <Info className="w-4 h-4" />, |
| 44 | + iconColor: 'text-blue-500', |
| 45 | + titleColor: 'text-blue-700 dark:text-blue-300', |
| 46 | + }, |
| 47 | + warning: { |
| 48 | + bg: 'bg-yellow-50 dark:bg-yellow-900/30', |
| 49 | + border: 'border-l-4 border-yellow-400', |
| 50 | + text: 'text-yellow-900 dark:text-yellow-100', |
| 51 | + icon: <AlertTriangle className="w-4 h-4" />, |
| 52 | + iconColor: 'text-yellow-500', |
| 53 | + titleColor: 'text-yellow-700 dark:text-yellow-200', |
| 54 | + }, |
| 55 | + danger: { |
| 56 | + bg: 'bg-red-50 dark:bg-red-900/30', |
| 57 | + border: 'border-l-4 border-red-400', |
| 58 | + text: 'text-red-900 dark:text-red-100', |
| 59 | + icon: <ShieldAlert className="w-4 h-4" />, |
| 60 | + iconColor: 'text-red-500', |
| 61 | + titleColor: 'text-red-700 dark:text-red-300', |
| 62 | + }, |
| 63 | + success: { |
| 64 | + bg: 'bg-green-50 dark:bg-green-900/30', |
| 65 | + border: 'border-l-4 border-green-400', |
| 66 | + text: 'text-green-900 dark:text-green-100', |
| 67 | + icon: <CheckCircle className="w-4 h-4" />, |
| 68 | + iconColor: 'text-green-500', |
| 69 | + titleColor: 'text-green-700 dark:text-green-300', |
| 70 | + }, |
| 71 | + note: { |
| 72 | + bg: 'bg-purple-50 dark:bg-purple-900/30', |
| 73 | + border: 'border-l-4 border-purple-400', |
| 74 | + text: 'text-purple-900 dark:text-purple-100', |
| 75 | + icon: <BadgeCheck className="w-4 h-4" />, |
| 76 | + iconColor: 'text-purple-500', |
| 77 | + titleColor: 'text-purple-700 dark:text-purple-300', |
| 78 | + }, |
| 79 | + tip: { |
| 80 | + bg: 'bg-cyan-50 dark:bg-cyan-900/30', |
| 81 | + border: 'border-l-4 border-cyan-400', |
| 82 | + text: 'text-cyan-900 dark:text-cyan-100', |
| 83 | + icon: <Sparkles className="w-4 h-4" />, |
| 84 | + iconColor: 'text-cyan-500', |
| 85 | + titleColor: 'text-cyan-700 dark:text-cyan-300', |
| 86 | + }, |
| 87 | + neutral: { |
| 88 | + bg: 'bg-gray-50 dark:bg-gray-900/30', |
| 89 | + border: 'border-l-4 border-gray-400', |
| 90 | + text: 'text-gray-900 dark:text-gray-100', |
| 91 | + icon: <CircleEllipsis className="w-4 h-4" />, |
| 92 | + iconColor: 'text-gray-500', |
| 93 | + titleColor: 'text-gray-700 dark:text-gray-300', |
| 94 | + }, |
| 95 | + quote: { |
| 96 | + bg: 'bg-indigo-50 dark:bg-indigo-900/30', |
| 97 | + border: 'border-l-4 border-indigo-400', |
| 98 | + text: 'text-indigo-900 dark:text-indigo-100', |
| 99 | + icon: <AlignLeft className="w-4 h-4" />, |
| 100 | + iconColor: 'text-indigo-500', |
| 101 | + titleColor: 'text-indigo-700 dark:text-indigo-300', |
| 102 | + }, |
16 | 103 | }; |
17 | 104 |
|
| 105 | +function extractTextFromChildren(children: ReactNode): string { |
| 106 | + if (typeof children === 'string') { |
| 107 | + return children; |
| 108 | + } |
| 109 | + if (Array.isArray(children)) { |
| 110 | + return children.map(extractTextFromChildren).join('\n'); |
| 111 | + } |
| 112 | + if (typeof children === 'object' && 'props' in children) { |
| 113 | + return extractTextFromChildren((children as any).props.children); |
| 114 | + } |
| 115 | + return ''; |
| 116 | +} |
| 117 | + |
| 118 | +function renderText(type: InfoPanelType, children: ReactNode) { |
| 119 | + const text = extractTextFromChildren(children); |
| 120 | + const lines = text |
| 121 | + .split(/\r?\n/) |
| 122 | + .reduce<string[]>((acc, line) => { |
| 123 | + const trimmed = line.trim(); |
| 124 | + if (!trimmed) return acc; |
| 125 | + |
| 126 | + const hasHtmlTag = /^<\w+/.test(trimmed); // <a ...>, <code>, <strong> 등 |
| 127 | + if (hasHtmlTag) { |
| 128 | + acc.push(trimmed); // 그대로 넣음 |
| 129 | + } else if (acc.length > 0 && !/^<\w+/.test(acc[acc.length - 1])) { |
| 130 | + acc[acc.length - 1] += ' ' + trimmed; // 이전 줄에 이어붙임 |
| 131 | + } else { |
| 132 | + acc.push(trimmed); |
| 133 | + } |
| 134 | + |
| 135 | + return acc; |
| 136 | + }, []); |
| 137 | + |
| 138 | + const [title, ...bodyLines] = lines; |
| 139 | + |
| 140 | + const style = typeStyles[type]; |
| 141 | + |
| 142 | + return ( |
| 143 | + <> |
| 144 | + <div className={`text-mi flex items-center gap-2 mt-2 mb-2 ${style.titleColor}`}> |
| 145 | + {React.cloneElement(style.icon, { className: `w-4 h-4 ${style.titleColor}` })} |
| 146 | + {title} |
| 147 | + </div> |
| 148 | + <div className={`space-y-1 text-mi pl-[1.3rem] ${style.titleColor}`}> |
| 149 | + {bodyLines.map((line, idx) => ( |
| 150 | + <div key={idx}>{line}</div> |
| 151 | + ))} |
| 152 | + </div> |
| 153 | + </> |
| 154 | + ); |
| 155 | +} |
| 156 | + |
18 | 157 | export default function InfoPanel({ type = 'info', children }: InfoPanelProps) { |
19 | | - const styles: Record<InfoPanelType, string> = { |
20 | | - info: 'bg-blue-50 border-l-4 border-blue-400 text-blue-900', |
21 | | - warning: 'bg-yellow-50 border-l-4 border-yellow-400 text-yellow-900', |
22 | | - danger: 'bg-red-50 border-l-4 border-red-400 text-red-900', |
23 | | - success: 'bg-green-50 border-l-4 border-green-400 text-green-900', |
24 | | - note: 'bg-purple-50 border-l-4 border-purple-400 text-purple-900', |
25 | | - tip: 'bg-cyan-50 border-l-4 border-cyan-400 text-cyan-900', |
26 | | - neutral: 'bg-gray-50 border-l-4 border-gray-400 text-gray-900', |
27 | | - quote: 'bg-indigo-50 border-l-4 border-indigo-400 text-indigo-900', |
28 | | - }; |
| 158 | + const style = typeStyles[type]; |
29 | 159 |
|
30 | 160 | return ( |
31 | | - <div className={`px-4 py-2 my-4 rounded ${styles[type]}`}> |
32 | | - <div className="prose-sm">{children}</div> |
33 | | - </div> |
| 161 | + <div className={`my-4 px-4 py-3 rounded-md prose ${style.bg} ${style.border} ${style.text}`}> |
| 162 | + {renderText(type, children)} |
| 163 | + </div> |
34 | 164 | ); |
35 | 165 | } |
0 commit comments