// ========================================================= // FBIF 资讯订阅 · Primitives // Base controls (from FBIF Ops kit) + domain helpers: // TierTag, QualityScore, ScoreRing, time formatting. // All values come from the design tokens — no invented sizes. // ========================================================= // Self-contained icon: React owns a stable ; the SVG is built // imperatively INSIDE it so Lucide never replaces a React-managed node // (avoids removeChild reconciliation crashes when icon-bearing lists re-render). const Icon = ({ name, size = 16, color, style }) => { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el || !window.lucide) return; el.innerHTML = ''; let svg = null; try { const pascal = String(name).split('-').map(s => s ? s[0].toUpperCase() + s.slice(1) : s).join(''); const node = window.lucide.icons && window.lucide.icons[pascal]; if (node && window.lucide.createElement) svg = window.lucide.createElement(node); } catch (e) { /* fall through */ } if (!svg) { const i = document.createElement('i'); i.setAttribute('data-lucide', name); el.appendChild(i); try { window.lucide.createIcons(); } catch (e) {} svg = el.querySelector('svg'); } else { el.appendChild(svg); } if (svg) { svg.setAttribute('width', size); svg.setAttribute('height', size); svg.setAttribute('stroke-width', 1.5); svg.style.width = size + 'px'; svg.style.height = size + 'px'; svg.style.color = color || 'currentColor'; if (style && style.fill) svg.style.fill = style.fill; } }, [name, size, color, style && style.fill]); return