/* =================================================================== KabelFlux v5 — BOM view + charts (L4 fix) - Donut: wires by gauge - Horizontal bars: cable-type distribution - KPI strip + line-item table =================================================================== */ /* — Reusable donut chart — */ const Donut = ({ data, size=180, thickness=22, label }) => { const total = data.reduce((a, b) => a + b.value, 0); const r = (size - thickness) / 2; const c = 2 * Math.PI * r; let acc = 0; return (
{data.map((d, i) => { const len = (d.value / total) * c; const slice = ( ); acc += len; return slice; })} {total} {(label || '').toUpperCase()}
{data.map((d, i) => (
{d.name} {d.value} {Math.round((d.value/total)*100)}%
))}
); }; /* — Horizontal bars — */ const BarRows = ({ data, max, format=(v=>v), unit='' }) => { const M = max || Math.max(...data.map(d => d.value)); return (
{data.map((d, i) => (
{d.name}
{format(d.value)}{unit}
))}
); }; /* — BOM view — */ const BomView = ({ wires=[], connectors=[], nodes=[], bundles=[], realPid=null, onExport }) => { const gaugeData = useMemo(() => { const map = {}; wires.forEach(w => { map[w.gauge] = (map[w.gauge] || 0) + 1; }); const palette = ['#4a9eff','#00d4aa','#f5a623','#a78bfa','#ec4899','#ef4e4e']; return Object.entries(map).sort((a,b) => b[1]-a[1]).map(([name, value], i) => ({ name, value, color: palette[i % palette.length] })); }, [wires]); const cableData = useMemo(() => { const map = {}; wires.forEach(w => { map[w.cable] = (map[w.cable] || 0) + 1; }); return Object.entries(map).sort((a,b)=>b[1]-a[1]).map(([name, value]) => ({ name, value })); }, [wires]); const colorData = useMemo(() => { const map = {}; wires.forEach(w => { map[w.color] = (map[w.color] || 0) + 1; }); return Object.entries(map).sort((a,b)=>b[1]-a[1]).slice(0,8).map(([name,value]) => ({ name, value, color: COLOR_HEX[name] })); }, [wires]); const lengthTotal = wires.reduce((a, w) => a + w.length, 0); const totalShield = wires.filter(w => w.shield).length; return (
Copy summary onExport && onExport('bom')}>Export PDF onExport && onExport('bom-xlsx')}>Export XLSX }/>
{/* KPI strip */}
{/* Charts row */}

Wires by gauge

{wires.length} total

Cable type distribution

{cableData.length} types

Color coverage

IEC 60757

Length by bundle

Active: L1
({ name: `${b.label} · ${b.count}w`, value: b.length, color: 'var(--info)' }))} unit=" mm"/>
{/* Connector schedule + Node schedule */}

Connector schedule

{connectors.length}
{connectors.map(c => { const wired = wires.filter(w => w.fromConn === c.id || w.toConn === c.id).length; return ( ); })}
RefTypeGenderPinsWiredDescription
{c.id} {c.type} {c.gender === 'M' ? 'Male' : 'Female'} {c.pins} {wired}/{c.pins} {c.label.split(' — ')[1]}

Node schedule

{nodes.length}
{nodes.map(n => ( ))}
RefTypeValueCoordinates
{n.label} {n.type === 'splice' ? 'Splice' : n.type === 'fuse' ? 'Fuse' : 'Ground'} {n.value || '—'} {n.x}, {n.y}
); }; const KPI = ({ label, value, sub, delta, tone='default' }) => (
{label}
{value}
{sub &&
{sub}
} {delta &&
{delta}
}
); const STYLE = ` .kf-bom-scroll { flex: 1; overflow-y: auto; padding: 18px; } .kf-kpi-strip { display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px; margin-bottom: 18px; } @media (max-width: 1200px) { .kf-kpi-strip { grid-template-columns: repeat(3, 1fr); } } .kf-kpi { background: var(--bg-2); border: 1px solid var(--line); border-radius: var(--r-3); padding: 14px 16px; } .kf-kpi-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--t-3); font-weight: 700; margin-bottom: 8px; } .kf-kpi-value { font-family: var(--f-display); font-size: 28px; font-weight: 700; color: var(--t-1); line-height: 1; letter-spacing: -.5px; } .kf-kpi-sub { font-size: 11px; color: var(--t-3); margin-top: 6px; } .kf-kpi-delta { font-size: 10.5px; color: var(--info); margin-top: 4px; font-family: var(--f-mono); } .kf-kpi-info .kf-kpi-value { color: var(--info); } .kf-kpi-success .kf-kpi-value { color: var(--success); } .kf-kpi-violet .kf-kpi-value { color: var(--violet); } .kf-kpi-warning .kf-kpi-value { color: var(--warning); } .kf-bom-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin-bottom: 14px; } .kf-bom-grid.two { grid-template-columns: 1.4fr 1fr; } @media (max-width: 1100px) { .kf-bom-grid, .kf-bom-grid.two { grid-template-columns: 1fr; } } /* Donut */ .kf-donut { display: flex; gap: 24px; align-items: center; } .kf-donut-legend { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; } .kf-donut-legend-row { display: grid; grid-template-columns: 14px 1fr auto 44px; align-items: center; gap: 10px; font-size: 12px; padding: 4px 0; } .kf-donut-dot { width: 10px; height: 10px; border-radius: 2px; } .kf-donut-label { color: var(--t-2); } .kf-donut-val { font-weight: 600; color: var(--t-1); } .kf-donut-pct { color: var(--t-3); text-align: right; } /* Bars */ .kf-bars { display: flex; flex-direction: column; gap: 8px; } .kf-bar-row { display: grid; grid-template-columns: 140px 1fr 60px; gap: 10px; align-items: center; font-size: 12px; } .kf-bar-name { color: var(--t-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .kf-bar-track { height: 14px; background: var(--bg-3); border-radius: 2px; overflow: hidden; position: relative; } .kf-bar-fill { height: 100%; border-radius: 2px; transition: width .35s var(--ease); } .kf-bar-val { color: var(--t-1); font-weight: 600; text-align: right; } `; document.head.appendChild(Object.assign(document.createElement('style'), { textContent: STYLE })); window.BomView = BomView; window.Donut = Donut; window.BarRows = BarRows; window.KPI = KPI;