/* =================================================================== KabelFlux v5 — Project home, Connectors, Settings, Empty =================================================================== */ /* ---------- Project home (NEW screen) ---------- Bento-style overview answering: "what is the state of this harness?" ============================================================ */ const ProjectHome = ({ project, onGoto, wires=[], connectors=[], nodes=[], bundles=[], realPid=null, onExport }) => { const wireCount = wires.length; const openWires = wires.filter(w => (w.notes || '').toLowerCase().includes('open')).length; const lastEdit = '2 min ago by L. Kohli'; const RECENT_EVENTS = [ { t: '2 min', who: 'L. Kohli', kind: 'edit', what: 'changed gauge on cavity 7 (22 → 20 AWG)' }, { t: '14 min', who: 'L. Kohli', kind: 'add', what: 'added wire cavity 18 · J2:6 → J3:6 · BK' }, { t: '1 h', who: 'R. Bedi', kind: 'review', what: 'marked rev B for engineering review' }, { t: '3 h', who: 'L. Kohli', kind: 'edit', what: 'updated J5 type to D-Sub DB-9' }, { t: '5 h', who: 'system', kind: 'backup', what: 'pre-deploy snapshot taken (224 KB)' }, { t: 'Yesterday', who: 'R. Bedi',kind: 'note', what: 'added revision note: "Shield drain wire to chassis ring per IPC-WHMA-A-620"' }, ]; return (
{project.code} {project.status === 'released' ? 'Released' : project.status === 'review' ? 'In review' : project.status === 'draft' ? 'Draft' : 'Active'} Rev {project.revision}

{project.name}

Last edit {lastEdit} · Owned by {project.owner}
Revision history Share onExport && onExport('prod')}>Production drawing
{/* Big harness preview */}

Layout preview

Schematic onGoto('layout')}>Open Layout
{/* Health card */}

Harness health

0 ? 'warning' : 'success'} dot>{openWires > 0 ? `${openWires} open` : 'OK'}
w.shield).length} total={wires.filter(w=>w.shield).length}/> a+c.pins,0)}/>
{/* Quick stats */}

At a glance

a+w.length,0)/1000).toFixed(2)} m`}/> w.shield).length}/>
{/* Activity feed */}

Recent activity

View all
{RECENT_EVENTS.map((e, i) => (
{e.t}
{e.who} {e.what}
))}
{/* Exports row */}

Recent exports

All formats
); }; /* Mini-layout for the Project Home preview card. - Reads real connectors/wires/nodes/bundles from props. - Computes a tight viewBox around the data + padding, lets the SVG scale to fit the card. - Empty-state placeholder when the project has no connectors yet. */ const MiniHarness = ({ wires=[], connectors=[], nodes=[], bundles=[] }) => { // Empty-state: no connectors → show a gentle "open the Layout view" // placeholder instead of a misleading generic harness diagram. if (connectors.length === 0 && nodes.length === 0) { return ( ); } // Compute bounding box from real connector + node positions, then pad. const points = [ ...connectors.map(c => ({ x: c.x || 0, y: c.y || 0 })), ...nodes.map(n => ({ x: n.x || 0, y: n.y || 0 })), ]; const xs = points.map(p => p.x); const ys = points.map(p => p.y); const PAD = 60; const minX = Math.min.apply(null, xs) - PAD; const minY = Math.min.apply(null, ys) - PAD; const maxX = Math.max.apply(null, xs) + PAD; const maxY = Math.max.apply(null, ys) + PAD; const vbW = Math.max(maxX - minX, 200); const vbH = Math.max(maxY - minY, 200); // Lookup table: wire endpoint names → {x,y}. Matches LayoutView's // `connectors.find(...) || nodes.find(...)` semantics so a wire that // terminates at a node still draws. const byName = {}; connectors.forEach(c => { byName[c.id] = c; }); nodes.forEach(n => { byName[n.id] = n; }); const wireD = (w) => { const from = byName[w.fromConn]; const to = byName[w.toConn]; if (!from || !to) return null; // Simple straight line for the preview — full LayoutView has bezier // + 90° formboard modes but this thumbnail doesn't need them. return `M ${from.x} ${from.y} L ${to.x} ${to.y}`; }; const COLOR_HEX = (typeof window !== 'undefined' && window.COLOR_HEX) || {}; return ( {/* Bundles — soft tinted bands underneath the wires */} {bundles.map((b, i) => ( b && b.from && b.to ? ( ) : null ))} {/* Wires — coloured by w.color, falls back to neutral grey */} {wires.map(w => { const d = wireD(w); if (!d) return null; const hex = COLOR_HEX[w.color] || '#888'; return ( ); })} {/* Nodes — small amber dots so they read as "junctions" */} {nodes.map(n => ( {n.label || n.id} ))} {/* Connectors — outer ring + perimeter pin dots + label. Pin count caps at 32 in the preview to keep the SVG light. */} {connectors.map(c => { const pins = Math.min(c.pins || 0, 32); const r = 22; const pinR = r - 7; return ( {Array.from({ length: pins }).map((_, i) => { const a = i / Math.max(pins, 1) * Math.PI * 2 - Math.PI / 2; return ( ); })} {c.id} ); })} ); }; const HealthRow = ({ label, done, total, warn=0 }) => { const pct = total > 0 ? (done / total) * 100 : 100; const ok = warn === 0 && done >= total; return (
{label}
{done}/{total} {warn > 0 && · {warn} warn}
); }; const QStat = ({ icon, label, v }) => (
{v}
{label}
); const ExportCard = ({ icon, label, sub, time, tone }) => ( ); /* ---------- Connectors view ---------- */ const ConnectorsView = ({ connectors=[], wires=[], loading=false, realPid=null, onRefresh }) => { const toast = useToast(); const [editing, setEditing] = useState(null); // connector being edited, or 'new' const [confirm, setConfirm] = useState(null); /* Click-to-expand: on narrow viewports only one card body is visible at a time. Initialise to the first connector so the user lands on a non-empty view. Toggling closes the current card; clicking a different card switches to it. */ const [expandedId, setExpandedId] = React.useState(null); React.useEffect(() => { if (expandedId == null && connectors.length) setExpandedId(connectors[0].id); }, [connectors, expandedId]); const toggleExpand = (id) => setExpandedId(prev => prev === id ? null : id); const openNew = () => setEditing({ id: '', name: '', type: '', description: '', pins: 8, gender: 'M', x: 100, y: 100, }); const openEdit = (c) => setEditing({ id: c.id, name: c.id, type: c.type || '', description: c.label && c.label.includes(' — ') ? c.label.split(' — ')[1] : '', pins: c.pins || 0, gender: c.gender || 'M', x: c.x || 100, y: c.y || 100, _raw: c._raw, }); const save = async (draft) => { try { if (draft._raw && draft._raw.id) { await kfxUpdateConnector(draft._raw.id, { id: draft.name, name: draft.name, type: draft.type, description: draft.description, pins: draft.pins, gender: draft.gender, x: draft.x, y: draft.y, }); toast.ok('Connector updated', draft.name); } else { if (!realPid) throw new Error('No project loaded'); await kfxCreateConnector(realPid, { id: draft.name, name: draft.name, type: draft.type, description: draft.description, pins: draft.pins, gender: draft.gender, x: draft.x, y: draft.y, }); toast.ok('Connector added', draft.name); } setEditing(null); if (onRefresh) await onRefresh(); } catch (e) { toast.err('Could not save connector', (e && e.message) || 'Server error'); } }; const del = (c) => { setConfirm({ title: `Delete connector ${c.id}?`, body: <>This removes {c.id} from the harness. Any wires terminating on it will become orphaned., onConfirm: async () => { if (!c._raw || !c._raw.id) { toast.err('No backend id for this connector'); return; } try { await kfxDeleteConnector(c._raw.id); toast.ok(`Deleted ${c.id}`); if (onRefresh) await onRefresh(); } catch (e) { toast.err('Could not delete connector', (e && e.message) || 'Server error'); } } }); }; if (loading && connectors.length === 0) { return (
Loading connectors…
); } if (connectors.length === 0) { return (
Symbol library New connector }/>
Add the first connector}/>
{editing && setEditing(null)} onSave={save}/>}
); } return (
Symbol library New connector }/>
{connectors.map(c => { const wired = wires.filter(w => w.fromConn === c.id || w.toConn === c.id).length; const pct = c.pins > 0 ? (wired / c.pins) * 100 : 0; const desc = c.label && c.label.includes(' — ') ? c.label.split(' — ')[1] : (c.type || ''); const isExpanded = expandedId === c.id; return (
{/* Header is a button-ish row: clicking the chip+type area toggles expansion; edit/trash stay separately clickable. */}
{ e && e.stopPropagation && e.stopPropagation(); openEdit(c); }}/> { e && e.stopPropagation && e.stopPropagation(); del(c); }}/>
Gender{c.gender === 'M' ? 'Male' : c.gender === 'F' ? 'Female' : '—'}
Pins{c.pins}
Wired{wired}/{c.pins}
{desc}
); })}
{editing && setEditing(null)} onSave={save}/>} {confirm && setConfirm(null)}/>}
); }; /* Simple connector edit/create modal — name + type + pins + gender. */ const ConnectorEditModal = ({ draft, onChange, onCancel, onSave }) => { const isNew = !(draft._raw && draft._raw.id); const setF = (k, v) => onChange({ ...draft, [k]: v }); const canSubmit = draft.name && draft.name.trim().length > 0 && draft.pins > 0; return ( Cancel { if (canSubmit) await onSave(draft); }}> {isNew ? 'Create connector' : 'Save changes'} }>
e.preventDefault()}>
setF('name', e.target.value)} placeholder="J1"/>
setF('pins', Math.max(1, +e.target.value || 1))}/>
setF('type', e.target.value)} placeholder="e.g. D38999 · MS3470 · DB9"/>
setF('description', e.target.value)}/>
); }; /* ---------- Nodes view ---------- */ const NodesView = ({ nodes=[], wires=[], loading=false, realPid=null, onRefresh }) => { const toast = useToast(); const [editing, setEditing] = useState(null); const [confirm, setConfirm] = useState(null); const openNew = () => setEditing({ id: '', label: '', type: 'splice', value: '', part_number: '', description: '', length: '', length_unit: 'mm', x: 300, y: 200, /* Wires that should route through this node. Mirrors backend wires.node_ref — we toggle each wire's node_ref to/from the node label on save. */ wireIds: [], }); const openEdit = (n) => setEditing({ id: n.id, label: n.label || n.id || '', type: n.type || 'splice', value: n.value || '', part_number: (n._raw && n._raw.part_number) || '', description: (n._raw && n._raw.description) || '', length: (n._raw && n._raw.length) || '', length_unit: 'mm', x: n.x || 300, y: n.y || 200, /* Seed wireIds from the wires whose backend node_ref equals this node's label. The user can then check more or uncheck some. */ wireIds: wires .filter(w => (w._raw && w._raw.node_ref) === (n.label || n.id)) .map(w => w._raw && w._raw.id) .filter(Boolean), _raw: n._raw, }); const save = async (draft) => { try { let savedNodeLabel = draft.label; if (draft._raw && draft._raw.id) { await kfxUpdateNode(draft._raw.id, draft); toast.ok('Node updated', draft.label); } else { if (!realPid) throw new Error('No project loaded'); await kfxCreateNode(realPid, draft); toast.ok('Node added', draft.label); } /* Sync wire-tags: any wire currently pointing at this node must either stay (still checked) or be cleared (unchecked); any newly- checked wire gets node_ref set to the node's label. Best-effort: a single failed wire write toasts the error but doesn't block the rest. */ const wantIds = new Set(draft.wireIds || []); const wasIds = new Set( wires.filter(w => (w._raw && w._raw.node_ref) === savedNodeLabel) .map(w => w._raw && w._raw.id) .filter(Boolean) ); const toAdd = [...wantIds].filter(id => !wasIds.has(id)); const toRemove = [...wasIds].filter(id => !wantIds.has(id)); for (const id of toAdd) { try { await kfxUpdateWire(id, { node_ref: savedNodeLabel }); } catch (e) { toast.err(`Could not tag wire #${id}`, (e && e.message) || ''); } } for (const id of toRemove) { try { await kfxUpdateWire(id, { node_ref: '' }); } catch (e) { toast.err(`Could not untag wire #${id}`, (e && e.message) || ''); } } setEditing(null); if (onRefresh) await onRefresh(); } catch (e) { toast.err('Could not save node', (e && e.message) || 'Server error'); } }; const del = (n) => { setConfirm({ title: `Delete node ${n.label || n.id}?`, body: <>This removes {n.label || n.id} from the harness., onConfirm: async () => { if (!n._raw || !n._raw.id) { toast.err('No backend id for this node'); return; } try { await kfxDeleteNode(n._raw.id); toast.ok(`Deleted ${n.label || n.id}`); if (onRefresh) await onRefresh(); } catch (e) { toast.err('Could not delete node', (e && e.message) || 'Server error'); } }, }); }; if (loading && nodes.length === 0) { return (
Loading nodes…
); } if (nodes.length === 0) { return (
Add a node }/>
Add the first node}/>
{editing && setEditing(null)} onSave={save}/>}
); } return (
Add a node }/>
{nodes.map(n => ( ))}
RefTypeValueXY
{n.label || n.id} {n.type} {n.value || '—'} {n.x} {n.y}
openEdit(n)}/> del(n)}/>
{editing && setEditing(null)} onSave={save}/>} {confirm && setConfirm(null)}/>}
); }; /* Node-type icon for the preview tile — matches the on-canvas glyph so the user gets immediate feedback when they change type. */ const NODE_TYPE_META = { splice: { label: 'Wire junction / fan-out point', glyph: '⊕' }, fuse: { label: 'Inline fuse', glyph: '⊟' }, ground: { label: 'Chassis ground', glyph: '⏚' }, jumper: { label: 'Jumper / strap', glyph: '⇄' }, }; const NodeEditModal = ({ draft, wires=[], onChange, onCancel, onSave }) => { const isNew = !(draft._raw && draft._raw.id); const setF = (k, v) => onChange({ ...draft, [k]: v }); const canSubmit = draft.label && draft.label.trim().length > 0; const wireIds = draft.wireIds || []; const toggleWire = (id) => { const set = new Set(wireIds); if (set.has(id)) set.delete(id); else set.add(id); setF('wireIds', [...set]); }; const selectAll = () => setF('wireIds', wires.map(w => w._raw && w._raw.id).filter(Boolean)); const deselectAll = () => setF('wireIds', []); const typeMeta = NODE_TYPE_META[draft.type] || NODE_TYPE_META.splice; return ( Cancel { if (canSubmit) await onSave(draft); }}> {isNew ? 'Save Node' : 'Save changes'} }>
e.preventDefault()}> {/* Row 1 — Node type + Label */}
setF('label', e.target.value)} placeholder="SP1, F1, R10, GND1…"/>
{/* Row 2 — Value + Part number */}
setF('value', e.target.value)} placeholder="e.g. 5A, 10kΩ, 100nF"/>
setF('part_number', e.target.value)} placeholder="e.g. LT1-5A"/>
{/* Row 3 — Description */}
setF('description', e.target.value)} placeholder="e.g. Main harness inline fuse"/>
{/* Row 4 — Length + unit */}
setF('length', e.target.value)} placeholder="e.g. 250" style={{ flex: 1 }}/>
{/* Row 5 — Wire tags (multi-select) */}
{wires.length === 0 && (
No wires yet — add wires first, then come back here to tag them.
)} {wires.map(w => { const id = w._raw && w._raw.id; if (!id) return null; const on = wireIds.indexOf(id) !== -1; const tag = ( (w.fromConn || '?') + ' · ' + (w.fromPin || '?') + ' → ' + (w.toConn || '?') + ' · ' + (w.toPin || '?') + ' · ' + (w.net || '—') + ' · ' + (w.color || '') + ' · ' + (w.gauge || '') ); return ( ); })}
{wires.length > 0 && (
✓ {wireIds.length} of {wires.length} wires tagged to this node
)}
{/* Preview tile */}
preview
{typeMeta.glyph}
{typeMeta.label}
{/* Row — X / Y (kept but tucked at the end since the modal puts label/type/value first the way the user expects) */}
setF('x', +e.target.value || 0)}/>
setF('y', +e.target.value || 0)}/>
); }; /* ---------- Settings (with proper IA — L3 fix) ---------- */ const SettingsView = ({ section='general' }) => { const [tab, setTab] = useState(section); return (
{tab === 'general' && } {tab === 'shortcuts' && } {tab === 'integrations' && } {tab === 'about' && }
); }; const GeneralSettings = () => ( <>

Profile

Localization

Used for BOM pricing, exports and timestamps.
Applied to BOM totals, supplier costs and quote exports.
Thousands separator and decimal mark.
); const ShortcutsSettings = () => (

Keyboard shortcuts

Editable
{[ ['Command palette', ['⌘','K']], ['New wire', ['N']], ['Toggle Wire mode', ['W']], ['Search current view', ['/']], ['Switch project', ['⌘','P']], ['Export production drawing',['⌘','⇧','E']], ['Toggle sidebar', ['⌘','\\']], ['Layout tab', ['G','1']], ['Wires tab', ['G','2']], ['BOM tab', ['G','3']], ['Undo / redo', ['⌘','Z']], ].map(([label, keys]) => (
{label} {keys.map((k,i) => {k})}
))}
); const IntegrationsSettings = () => (

Connected systems

{[ { name:'Komax / Schleuniger', status:'connected', sub:'KX-V machine · last sync 2 h ago' }, { name:'AutoCAD / Fusion 360', status:'connected', sub:'DXF export ready' }, { name:'Jira (engineering)', status:'disconnect',sub:'Connect to link revisions to issues' }, { name:'PLM (Teamcenter)', status:'disconnect',sub:'Map title block to part numbers' }, ].map(it => (
{it.name}
{it.sub}
{it.status === 'connected' ? Connected : Connect}
))}
); const AboutSettings = () => (

About

KabelFlux e-Harness Suite · v5.0 UX refresh
A wiring-harness design and documentation tool for aerospace engineers.

Compliant outputs: AS50881 conventions, IPC-WHMA-A-620, IEC 60757 colour codes, Komax / Schleuniger wire-prep CSV, AutoCAD DXF.
); /* ---------- Project Settings (per-project, sidebar entry) ---------- Per-project metadata, defaults, access, and danger zone. Everything personal / workspace-wide (theme, locale, currency, profile) lives in the global Settings view instead. */ const ProjectSettingsView = ({ project }) => { const toast = useToast(); const [section, setSection] = useState('overview'); // Team state — keeps which member is the Owner so we can enforce // "only one Owner at a time". Picking "Transfer ownership…" on any // non-Owner row moves the Owner role to that member and the previous // Owner falls back to Designer. const [team, setTeam] = useState([ { id:'lk', name:'Laila Kohli', email:'laila@ikran.aero', role:'Owner', last:'now' }, { id:'rb', name:'Reza Bedi', email:'reza@ikran.aero', role:'Approver', last:'12 min ago' }, { id:'so', name:'Sam Okonkwo', email:'sam.o@ikran.aero', role:'Reviewer', last:'2 h ago' }, { id:'nl', name:'Noemi Lazar', email:'noemi@ikran.aero', role:'Designer', last:'2 d ago' }, { id:'ap', name:'Aiden Park', email:'aiden@ikran.aero', role:'Viewer', last:'yesterday' }, ]); const [pendingTransfer, setPendingTransfer] = useState(null); // {id, name} const setMemberRole = (id, role) => { if (role === '__transfer__') { const target = team.find(m => m.id === id); setPendingTransfer({ id, name: target.name }); return; } setTeam(t => t.map(m => m.id === id ? { ...m, role } : m)); }; const confirmTransfer = () => { setTeam(t => t.map(m => { if (m.id === pendingTransfer.id) return { ...m, role: 'Owner' }; if (m.role === 'Owner') return { ...m, role: 'Designer' }; return m; })); toast.ok('Ownership transferred', `${pendingTransfer.name} is now the Owner`); setPendingTransfer(null); }; const sections = [ { k:'overview', l:'Overview', icon:'folder' }, { k:'titleblock',l:'Title block', icon:'title' }, { k:'team', l:'Team & access', icon:'user' }, { k:'defaults', l:'Harness defaults', icon:'wires' }, { k:'compliance',l:'Compliance', icon:'check' }, { k:'exports', l:'Exports & sync', icon:'share' }, { k:'danger', l:'Danger zone', icon:'warn' }, ]; return (
Settings for {project.name} ({project.code}). For personal preferences, currency or theme, open Settings from your avatar menu.} actions={null}/>
{section === 'overview' && (

Project metadata