|
1 | | -import React, { ReactNode } from 'react'; |
| 1 | +import React, {isValidElement, ReactNode} from 'react'; |
2 | 2 | import { |
3 | 3 | Info, |
4 | 4 | AlertTriangle, |
@@ -102,58 +102,71 @@ const typeStyles: Record< |
102 | 102 | }, |
103 | 103 | }; |
104 | 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 | | -} |
| 105 | +function extractLines(children: ReactNode): ReactNode[][] { |
| 106 | + const result: ReactNode[][] = []; |
| 107 | + let line: ReactNode[] = []; |
117 | 108 |
|
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; |
| 109 | + const flush = () => { |
| 110 | + if (line.length > 0) { |
| 111 | + result.push(line); |
| 112 | + line = []; |
| 113 | + } |
| 114 | + }; |
125 | 115 |
|
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); |
| 116 | + const walk = (node: ReactNode) => { |
| 117 | + if (typeof node === 'string') { |
| 118 | + node.split(/\r?\n/).forEach((part, i) => { |
| 119 | + if (i > 0) { |
| 120 | + flush(); |
133 | 121 | } |
| 122 | + if (part) { |
| 123 | + line.push(part); |
| 124 | + } |
| 125 | + }); |
| 126 | + } else if (Array.isArray(node)) { |
| 127 | + node.forEach((child) => { |
| 128 | + walk(child); |
| 129 | + }); |
| 130 | + } else if (isValidElement(node)) { |
| 131 | + const { type, props } = node as any; |
| 132 | + if (type === 'a' || type === 'code' || type === 'b') { |
| 133 | + line.push(node); |
| 134 | + } else { |
| 135 | + walk(props.children); |
| 136 | + } |
| 137 | + } else if (typeof node === 'number') { |
| 138 | + line.push(node); |
| 139 | + } |
| 140 | + }; |
134 | 141 |
|
135 | | - return acc; |
136 | | - }, []); |
| 142 | + walk(children); |
| 143 | + flush(); |
| 144 | + return result; |
| 145 | +} |
137 | 146 |
|
138 | | - const [title, ...bodyLines] = lines; |
139 | 147 |
|
| 148 | +function renderText(type: InfoPanelType, children: ReactNode) { |
| 149 | + const lines = extractLines(children); |
| 150 | + const [title = [], ...rest] = lines; |
140 | 151 | const style = typeStyles[type]; |
141 | 152 |
|
142 | 153 | return ( |
143 | 154 | <> |
144 | | - <div className={`text-mi flex items-center gap-2 mt-2 mb-2 ${style.titleColor}`}> |
| 155 | + <div className={`text-mi flex items-center gap-2 mt-2 mb-1 ${style.titleColor}`}> |
145 | 156 | {React.cloneElement(style.icon, { className: `w-4 h-4 ${style.titleColor}` })} |
146 | 157 | {title} |
147 | 158 | </div> |
148 | 159 | <div className={`space-y-1 text-mi pl-[1.3rem] ${style.titleColor}`}> |
149 | | - {bodyLines.map((line, idx) => ( |
150 | | - <div key={idx}>{line}</div> |
| 160 | + {rest.map((line, i) => ( |
| 161 | + <div key={i}>{line}</div> |
151 | 162 | ))} |
152 | 163 | </div> |
153 | 164 | </> |
154 | 165 | ); |
155 | 166 | } |
156 | 167 |
|
| 168 | + |
| 169 | + |
157 | 170 | export default function InfoPanel({ type = 'info', children }: InfoPanelProps) { |
158 | 171 | const style = typeStyles[type]; |
159 | 172 |
|
|
0 commit comments