// H2Learn — shared UI primitives. Plain JSX, no globals collision. const { useState, useMemo, useRef, useEffect } = React; const Icon = ({name, size=16, stroke=2, color='currentColor', style}) => { // tiny inline icon set — feather-style const paths = { search: 'M21 21l-4.35-4.35M11 19a8 8 0 100-16 8 8 0 000 16z', user: 'M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2 M16 7a4 4 0 11-8 0 4 4 0 018 0z', network: 'M12 3v3m0 12v3M3 12h3m12 0h3M5.6 5.6l2.1 2.1m8.6 8.6l2.1 2.1M5.6 18.4l2.1-2.1m8.6-8.6l2.1-2.1M9 12a3 3 0 106 0 3 3 0 00-6 0z', sparkles: 'M12 3l1.8 4.2 4.2 1.8-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8L12 3z M19 14l.9 2.1L22 17l-2.1.9L19 20l-.9-2.1L16 17l2.1-.9L19 14z', grid: 'M3 3h7v7H3zm11 0h7v7h-7zM3 14h7v7H3zm11 0h7v7h-7z', layers: 'M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5', bell: 'M6 8a6 6 0 1112 0c0 7 3 9 3 9H3s3-2 3-9zM10.3 21a1.94 1.94 0 003.4 0', settings: 'M12 15a3 3 0 100-6 3 3 0 000 6z M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z', plus: 'M12 5v14M5 12h14', x: 'M18 6L6 18M6 6l12 12', check: 'M20 6L9 17l-5-5', arrowRight: 'M5 12h14M12 5l7 7-7 7', arrowLeft: 'M19 12H5M12 19l-7-7 7-7', chevronRight: 'M9 18l6-6-6-6', chevronLeft: 'M15 18l-6-6 6-6', chevronDown: 'M6 9l6 6 6-6', chevronUp: 'M18 15l-6-6-6 6', info: 'M12 16v-4M12 8h.01M22 12a10 10 0 11-20 0 10 10 0 0120 0z', alert: 'M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0zM12 9v4M12 17h.01', star: 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z', bookmark: 'M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2v16z', send: 'M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z', eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z M12 15a3 3 0 100-6 3 3 0 000 6z', book: 'M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2zM22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z', trending: 'M23 6l-9.5 9.5-5-5L1 18M17 6h6v6', map: 'M1 6v16l7-4 8 4 7-4V2l-7 4-8-4-7 4z M8 2v16M16 6v16', zap: 'M13 2L3 14h9l-1 8 10-12h-9l1-8z', home: 'M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2zM9 22V12h6v10', compass: 'M12 22a10 10 0 100-20 10 10 0 000 20z M16.24 7.76l-2.12 6.36-6.36 2.12 2.12-6.36 6.36-2.12z', bridge: 'M3 12h18M5 12V8a3 3 0 016 0v4M13 12V8a3 3 0 016 0v4M3 19h18', flask: 'M9 2h6v5l5 11a2 2 0 01-2 3H6a2 2 0 01-2-3L9 7V2z M9 11h6', target: 'M12 22a10 10 0 100-20 10 10 0 000 20z M12 18a6 6 0 100-12 6 6 0 000 12z M12 14a2 2 0 100-4 2 2 0 000 4z', git: 'M6 3v12 M18 9a3 3 0 100-6 3 3 0 000 6z M6 21a3 3 0 100-6 3 3 0 000 6z M18 9a9 9 0 01-9 9', sliders: 'M4 21V14M4 10V3M12 21V12M12 8V3M20 21v-5M20 12V3M1 14h6M9 8h6M17 16h6', download: 'M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4 M7 10l5 5 5-5 M12 15V3', bot: 'M12 8V4H8 M2 14h2 M20 14h2 M15 13a2 2 0 100-4 2 2 0 000 4z M9 13a2 2 0 100-4 2 2 0 000 4z M5 22h14a2 2 0 002-2v-8a4 4 0 00-4-4H7a4 4 0 00-4 4v8a2 2 0 002 2z', arrowUpRight: 'M7 17L17 7M7 7h10v10', }; const d = paths[name] || paths.info; return ( ); }; const Btn = ({children, onClick, variant='', size='', icon, iconRight, className='', style, disabled}) => ( ); const Chip = ({children, variant='', size='', dot=false, className='', style, onClick}) => ( {children} ); const Card = ({children, variant='', className='', style, onClick}) => (
{children}
); const Avatar = ({initials, size='', kind='person', tone, style, className=''}) => { const tones = { person: { bg: 'var(--accent-soft)', fg: 'var(--accent)' }, project:{ bg: 'var(--warm-soft)', fg: 'var(--warm)' }, org: { bg: 'var(--moss-soft)', fg: 'var(--moss)' } }; const t = tone || tones[kind] || tones.person; return ( {initials} ); }; const Badge = ({children, level='', className='', style}) => ( {children} ); const Bar = ({value=0, max=100, variant='', height='thin', label, showVal=true}) => (
{label &&
{label}{showVal && {value}{max===100?'%':''}}
}
); const ScoreRing = ({value=0, size=88, stroke=8, color='var(--accent)', sub='match'}) => { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const dash = c * (value/100); return (
{value}%{sub}
); }; // Relationship type → label/color/icon const RelMeta = { Similarity: { color: 'var(--e-similar)', chip: 'rel-similar', icon: 'sparkles' }, Complementarity: { color: 'var(--e-comp)', chip: 'rel-comp', icon: 'git' }, 'Anti-duplication': { color: 'var(--e-antidup)', chip: 'rel-antidup', icon: 'alert' }, Bridge: { color: 'var(--e-bridge)', chip: 'rel-bridge', icon: 'bridge' }, 'Method transfer': { color: 'var(--e-method)', chip: 'rel-method', icon: 'flask' }, 'Shared uncertainty': { color: 'var(--e-uncertain)', chip: 'rel-uncertain', icon: 'compass' }, 'Need-offer': { color: 'var(--e-needoffer)', chip: 'rel-needoffer', icon: 'arrowRight' }, 'Project compatibility': { color: 'var(--e-comp)', chip: 'rel-comp', icon: 'target' }, }; const RelChip = ({type, size}) => { const m = RelMeta[type] || RelMeta.Similarity; return {type}; }; // Entity colors (nodes) const NodeColor = { person: 'var(--c-person)', project: 'var(--c-project)', org: 'var(--c-org)', domain: 'var(--c-domain)', method: 'var(--c-method)', challenge: 'var(--c-challenge)', bottleneck: 'var(--c-bottleneck)', uncertainty: 'var(--c-uncertainty)', need: 'var(--c-need)', offer: 'var(--c-offer)', }; // Entity row — used in sidebar lists, detail drawer "top connections", etc. const EntityRow = ({entity, kind='person', meta, action, onClick, selected}) => { const e = entity; return (
{e.name}
{e.role || e.type || e.sector || ''}{e.org ? ' · ' + e.org : ''}
{meta &&
{meta}
} {action}
); }; // Helper: format a fingerprint as horizontal weighted bars const Fingerprint = ({data, max=50, color='var(--accent)', compact=false}) => (
{data.map((row, i) => (
{row.d}
{row.w}
))}
); // useNav — tiny router using location.hash#screen function useNav() { const [screen, setScreenState] = useState(() => (location.hash.replace('#','') || 'landing')); useEffect(() => { const h = () => setScreenState(location.hash.replace('#','') || 'landing'); window.addEventListener('hashchange', h); return () => window.removeEventListener('hashchange', h); }, []); const go = (s) => { location.hash = s; window.scrollTo(0,0); }; return [screen, go]; } Object.assign(window, { Icon, Btn, Chip, Card, Avatar, Badge, Bar, ScoreRing, RelMeta, RelChip, NodeColor, EntityRow, Fingerprint, useNav, });