// 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',
minus: 'M5 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', caption=null, showCaption=false}) => {
const r = (size - stroke) / 2;
const c = 2 * Math.PI * r;
const dash = c * (value/100);
// number scales with ring size so it never overflows
const numSize = Math.round(size * 0.36);
const ring = (
);
if (!showCaption && !caption) return ring;
return (
{ring}
{caption || sub || 'match score'}
);
};
// === Global prototype toast ===
// Usage: window.proto('Saved to shortlist') or window.proto('msg', { tag: 'simulated' })
(function setupToast(){
if (window.proto) return;
const wrap = document.createElement('div');
wrap.className = 'ptoast-wrap';
document.body.appendChild(wrap);
window.proto = (msg, opts={}) => {
const t = document.createElement('div');
t.className = 'ptoast';
const tag = opts.tag || 'prototype';
t.innerHTML = `${tag}`;
t.lastChild.textContent = msg;
wrap.appendChild(t);
const ttl = opts.ttl || 2400;
setTimeout(() => { t.classList.add('leave'); setTimeout(() => t.remove(), 220); }, ttl);
};
})();
const proto = (...a) => window.proto(...a);
// 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) => (
))}
);
// 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, proto,
});