/* ===================================================================
KabelFlux v5 β Layout view (consolidated Diagram + Formboard, per Day 2)
- Chip toolbar grouped: Wires / Labels / Detail (per DEMO_SCRIPT Part 2)
- Pin face Z-order corrected (H2 fix): faces drawn UNDER labels
- Reduced motion respected (H5)
- Wire mode banner (per Part 4.5)
=================================================================== */
const SYM = {
Circular: (props) => {
const { x, y, pins, gender, sel, showPinNums=true } = props;
const r = 38;
return (
{Array.from({ length: pins }).map((_, i) => {
const a = (i / pins) * Math.PI * 2 - Math.PI / 2;
const cx = Math.cos(a) * (r - 14);
const cy = Math.sin(a) * (r - 14);
return (
{gender === 'M'
?
: }
{showPinNums && (
{i + 1}
)}
);
})}
);
},
Rect: (props) => {
const { x, y, pins, gender, sel, w=78, h=56, showPinNums=true } = props;
const rows = Math.min(2, Math.ceil(pins / 4));
const cols = Math.ceil(pins / rows);
return (
{Array.from({ length: pins }).map((_, i) => {
const col = i % cols, row = Math.floor(i / cols);
const cx = -w/2 + 14 + col * ((w - 28) / Math.max(1, cols - 1));
const cy = rows === 1 ? 0 : -h/2 + 14 + row * (h - 28);
return (
{gender === 'M'
?
: }
{showPinNums && (
{i + 1}
)}
);
})}
);
}
};
const NODE_GLYPH = {
splice: (col) => ,
fuse: (col) => ,
ground: (col) => ,
};
/* β Canvas β */
const LayoutView = ({ wireMode, setWireMode, renderMode, density, wires=[], connectors=[], nodes=[], bundles=[], loading=false, project, toast }) => {
// Analyse β consistency check against the backend's audit_project rules.
// Same engine as the old "π Audit" button in the legacy SPA, just
// surfaced here next to Wire mode where designers will actually find it.
const [analyseRunning, setAnalyseRunning] = useState(false);
const [analyseResult, setAnalyseResult] = useState(null); // { issues, projectName }
const runAnalyse = async () => {
if (!project || !project.realId) {
toast && toast.warn && toast.warn('Open a project first to analyse it.');
return;
}
setAnalyseRunning(true);
try {
// Call the backend's audit endpoint directly β same engine as the
// legacy SPA's old "π Audit" button.
const resp = await window.kfxFetch('/projects/' + project.realId + '/audit');
const issues = (resp && Array.isArray(resp.issues)) ? resp.issues : [];
setAnalyseResult({ issues: issues, projectName: project.name || '' });
const n = (issues || []).length;
const errs = (issues || []).filter(i => i.severity === 'error').length;
const warns = (issues || []).filter(i => i.severity === 'warning').length;
if (n === 0) toast && toast.success && toast.success('Analyse Β· no issues found.');
else toast && toast.info && toast.info('Analyse Β· ' + errs + ' error' + (errs===1?'':'s') + ', ' + warns + ' warning' + (warns===1?'':'s') + ' found.');
} catch (e) {
toast && toast.err && toast.err('Analyse failed: ' + (e.message || e));
} finally {
setAnalyseRunning(false);
}
};
const [tools, setTools] = useState({
nets: true, nodes: true, gauges: false, callouts: true,
pinFaces: true, shieldGlow: true, tables: false, dims: true, grid: true, bezier: true,
});
const [zoom, setZoom] = useState(1);
const [sel, setSel] = useState(null);
const [hover, setHover] = useState(null);
const reducedMotion = useRef(typeof matchMedia !== 'undefined' && matchMedia('(prefers-reduced-motion: reduce)').matches);
const toggle = k => setTools(t => ({ ...t, [k]: !t[k] }));
/* Wire paths */
const wirePath = (w) => {
const from = connectors.find(c => c.id === w.fromConn) || nodes.find(n => n.id === w.fromConn);
const to = connectors.find(c => c.id === w.toConn) || nodes.find(n => n.id === w.toConn);
if (!from || !to) return null;
if (renderMode === 'formboard') {
// 90Β° formboard routing
const midX = (from.x + to.x) / 2;
return `M ${from.x} ${from.y} L ${midX} ${from.y} L ${midX} ${to.y} L ${to.x} ${to.y}`;
}
if (!tools.bezier) {
// straight line when Bezier is toggled off
return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;
}
// schematic bezier
const dx = (to.x - from.x) * 0.4;
return `M ${from.x} ${from.y} C ${from.x + dx} ${from.y}, ${to.x - dx} ${to.y}, ${to.x} ${to.y}`;
};
return (
{/* Chip toolbar β grouped per Day 2 */}
Wires
toggle('bezier')} icon="bundle">Bezier
toggle('shieldGlow')}>Shield glow
Labels
toggle('nets')}>Nets
toggle('nodes')}>Nodes
toggle('gauges')}>Gauges
toggle('callouts')}>Callouts
Detail
toggle('pinFaces')}>Pin faces
toggle('dims')} icon="ruler">Dimensions
toggle('grid')} icon="grid">Grid
{/* Wire mode toggle β same format as the other toolbar chips. */}
Action
setWireMode(!wireMode)}
icon="bolt" ariaLabel="Toggle wire-creation mode">
{wireMode ? 'Wire mode β’ ON' : 'Wire mode'}
{/* Analyse β runs the backend consistency audit, formerly the "Audit"
button in the legacy SPA. Surfaces unknown connectors, duplicate
wires, pin overruns, etc. */}
{analyseRunning ? 'Analysingβ¦' : 'Analyse'}
{/* Wire-mode hint banner */}
{wireMode && (
Wire mode active β click pin A, then pin B to create a wire. Esc to exit.
)}
{/* Left rail: zoom + render mode */}
setZoom(z => Math.min(2, z + .15))}/>
setZoom(z => Math.max(.4, z - .15))}/>
{Math.round(zoom * 100)}%
toggle('grid')}/>
{/* The SVG canvas */}
{/* Right inspector */}
{sel ? (
{(() => {
const c = connectors.find(x => x.id === sel);
if (!c) return null;
const wiresOn = wires.filter(w => w.fromConn === sel || w.toConn === sel);
return (
<>
{c.gender === 'M' ? 'Male' : 'Female'}
Pinout Β· {wiresOn.length} wired
{Array.from({ length: c.pins }).map((_, i) => {
const w = wiresOn.find(w => (w.fromConn === sel && w.fromPin === i + 1) || (w.toConn === sel && w.toPin === i + 1));
return (
{i + 1}
{w ? (
<>
{w.net}
β {w.fromConn === sel ? `${w.toConn}:${w.toPin}` : `${w.fromConn}:${w.fromPin}`}
>
) : (
open
)}
);
})}
Edit
Duplicate
>
);
})()}
) : (
)}
{/* Analyse results β same modal layout as the legacy SPA's audit modal,
restyled to the bundle's design language. */}
{analyseResult && (
setAnalyseResult(null)}
footer={ setAnalyseResult(null)}>Close}>
{analyseResult.issues.length === 0 ? (
β
No issues found
This harness looks clean.
) : (
{/* Severity summary */}
{[['error','danger','π΄'],['warning','warning','π‘'],['info','info','π΅']].map(([sev, tone, dot]) => {
const n = analyseResult.issues.filter(i => i.severity === sev).length;
if (!n) return null;
return (
{dot}{n} {sev}{n!==1?'s':''}
);
})}
{analyseResult.issues.length} total issue{analyseResult.issues.length!==1?'s':''}
{/* Issue list */}
{analyseResult.issues.map((issue, i) => (
{issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARN' : 'INFO'}
[{issue.category || 'β'}] {issue.message}
{issue.fix && (
π‘ {issue.fix}
)}
))}
)}
)}
);
};
/* ---------- Styles ---------- */
const LAYOUT_STYLE = `
.kf-layout { display: flex; flex-direction: column; height: 100%; }
.kf-canvas-bar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 10px 14px;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
}
.kf-bar-group { display: inline-flex; align-items: center; gap: 5px; }
.kf-bar-label {
font-size: 9.5px; font-weight: 700; text-transform: uppercase;
letter-spacing: 1px; color: var(--t-3); margin-right: 4px;
}
.kf-banner {
display: flex; align-items: center; gap: 10px;
padding: 8px 14px;
font-size: 12.5px;
border-bottom: 1px solid var(--line);
}
.kf-banner b { color: inherit; font-weight: 700; }
.kf-banner-success { background: var(--success-soft); color: var(--success); }
.kf-banner-warning { background: var(--warning-soft); color: var(--warning); }
.kf-banner-info { background: var(--info-soft); color: var(--info); }
.kf-canvas-wrap {
display: grid;
grid-template-columns: 44px 1fr 280px;
flex: 1;
min-height: 0;
}
.kf-canvas-rail {
display: flex; flex-direction: column; align-items: center; gap: 4px;
padding: 8px 4px;
background: var(--bg-2);
border-right: 1px solid var(--line);
}
.kf-rail-zoom { font-size: 10px; color: var(--t-3); padding: 4px 0; }
.kf-rail-sep { width: 24px; height: 1px; background: var(--line); margin: 6px 0; }
.kf-canvas {
overflow: auto;
position: relative;
}
.kf-canvas-inspector {
background: var(--bg-2);
border-left: 1px solid var(--line);
display: flex; flex-direction: column;
overflow: hidden;
}
.kf-inspector-head {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
}
.kf-inspector-title { font-size: 11px; font-weight: 700; letter-spacing: .6px; text-transform: uppercase; color: var(--t-3); }
.kf-inspector-body { padding: 14px; flex: 1; overflow-y: auto; }
.kf-inspector-field { display: flex; flex-direction: column; gap: 3px; margin-bottom: 12px; }
.kf-inspector-field.flex { flex: 1; }
.kf-inspector-field label { font-size: 9.5px; text-transform: uppercase; letter-spacing: .6px; color: var(--t-3); font-weight: 700; }
.kf-inspector-value { font-size: 12.5px; color: var(--t-1); }
.kf-inspector-value.num { font-family: var(--f-mono); }
.kf-inspector-row { display: flex; gap: 12px; }
.kf-inspector-sep { height: 1px; background: var(--line); margin: 6px 0 12px; }
.kf-inspector-label { font-size: 10px; text-transform: uppercase; letter-spacing: .8px; color: var(--t-3); font-weight: 700; margin-bottom: 8px; }
.kf-inspector-actions { display: flex; gap: 6px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }
.kf-pinout {
display: flex; flex-direction: column; gap: 2px;
background: var(--bg-1);
border: 1px solid var(--line);
border-radius: var(--r-2);
padding: 4px;
}
.kf-pinout-row {
display: grid;
grid-template-columns: 22px 14px 1fr auto;
gap: 8px; align-items: center;
padding: 4px 6px;
border-radius: 3px;
font-size: 11px;
}
.kf-pinout-row:hover { background: var(--bg-3); }
.kf-pinout-cav { color: var(--t-3); font-weight: 600; text-align: right; }
.kf-pinout-sw { width: 14px; height: 6px; border-radius: 1px; }
.kf-pinout-net { color: var(--t-1); font-family: var(--f-mono); font-size: 11px; }
.kf-pinout-to { color: var(--t-3); }
.kf-pinout-empty{ color: var(--t-4); font-style: italic; font-size: 10.5px; }
/* β Analyse-results modal β */
.kf-analyse-summary {
display: flex; align-items: center; gap: 8px;
padding: 8px 12px;
background: var(--bg-3);
border: 1px solid var(--line);
border-radius: var(--r-3);
margin-bottom: 12px;
font-size: 12px; font-weight: 600;
}
.kf-analyse-pill {
display: inline-flex; align-items: center;
padding: 2px 8px;
border-radius: var(--r-pill);
font-size: 11px; font-weight: 700;
}
.kf-analyse-pill-danger { background: rgba(239,78,78,.12); color: var(--danger); }
.kf-analyse-pill-warning { background: var(--warning-soft); color: var(--warning); }
.kf-analyse-pill-info { background: var(--info-soft); color: var(--info); }
.kf-analyse-list { max-height: 50vh; overflow-y: auto; }
.kf-analyse-row {
display: flex; gap: 10px; align-items: flex-start;
padding: 8px 12px;
border: 1px solid var(--line);
border-left-width: 3px;
border-radius: var(--r-2);
margin-bottom: 6px;
background: var(--bg-2);
}
.kf-analyse-row-error { border-left-color: var(--danger); background: rgba(239,78,78,.04); }
.kf-analyse-row-warning { border-left-color: var(--warning); background: rgba(245,166,35,.05); }
.kf-analyse-row-info { border-left-color: var(--info); background: rgba(74,158,255,.04); }
.kf-analyse-sev {
flex-shrink: 0;
font-size: 10px; font-weight: 800;
letter-spacing: 0.5px;
color: var(--t-3);
min-width: 44px; padding-top: 2px;
}
.kf-analyse-row-error .kf-analyse-sev { color: var(--danger); }
.kf-analyse-row-warning .kf-analyse-sev { color: var(--warning); }
.kf-analyse-row-info .kf-analyse-sev { color: var(--info); }
.kf-analyse-body { flex: 1; min-width: 0; }
.kf-analyse-msg { font-size: 12.5px; color: var(--t-1); line-height: 1.4; }
.kf-analyse-cat { font-family: var(--f-mono); font-size: 11px; color: var(--t-2); font-weight: 700; }
.kf-analyse-fix { font-size: 11.5px; color: var(--t-3); margin-top: 4px; }
`;
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: LAYOUT_STYLE }));
window.LayoutView = LayoutView;