/* =================================================================== KabelFlux v5 — Wires view - Inline-edit cells w/ save-flash + error-shake (M4 fix) - Color cells pair color + IEC 2-letter code (M3 — no color-only signal) - Density-aware row height (driven by --density-row) - Status column with icon + word for open wires (M3) - Sticky header + sticky first column =================================================================== */ /* Specialized color cell: shows swatch + IEC code + name; click → inline select */ const ColorCell = ({ value, onSave }) => { const [editing, setEditing] = useState(false); const [v, setV] = useState(value); const [flash, setFlash] = useState(null); const ref = useRef(); useEffect(() => setV(value), [value]); useEffect(() => { if (editing && ref.current) ref.current.focus(); }, [editing]); const commit = async (newV) => { if (newV === value) { setEditing(false); return; } try { await onSave(newV); setEditing(false); setFlash('ok'); setTimeout(() => setFlash(null), 1100); } catch (e) { setFlash('err'); setTimeout(() => setFlash(null), 400); } }; const cls = 'kf-cell color-cell' + (editing ? ' editing' : '') + (flash === 'ok' ? ' flash-save' : '') + (flash === 'err' ? ' shake-err' : ''); if (editing) { return ( ); } return ( setEditing(true)} tabIndex={0} onKeyDown={e => { if (e.key === 'Enter' || e.key === 'F2') setEditing(true); }}>
{COLOR_CODE[value]} {value}
); }; const WiresView = ({ density='cozy', wires=[], connectors=[], loading=false, realPid=null, onRefresh, onImportCsv }) => { const toast = useToast(); const [data, setData] = useState(wires); // Re-sync local state whenever the project's wire list changes. useEffect(() => { setData(wires); }, [wires]); const [filter, setFilter] = useState(''); const [filterColor, setFilterColor] = useState(''); const [filterShield, setFilterShield] = useState(false); const [confirm, setConfirm] = useState(null); const [selRow, setSelRow] = useState(null); const [showCreate, setShowCreate] = useState(false); const save = (id, field) => async (value) => { const w = data.find(x => x.id === id); if (!w || !w._raw || !w._raw.id) throw new Error('Wire has no backend id'); // Build a complete wire object with the patched field so we PUT the // full row (backend's update_wire replaces all columns). const patched = { ...w, [field]: value }; try { await kfxUpdateWire(w._raw.id, patched); setData(d => d.map(x => x.id === id ? patched : x)); toast.ok(`Saved · ${field}`, `Wire ${w.fromConn}:${w.fromPin} → ${w.toConn}:${w.toPin} updated`); } catch (e) { toast.err(`Could not save ${field}`, (e && e.message) || 'Server error'); throw e; } }; const del = (w) => { setConfirm({ title: `Delete wire ${w.cavity} (${w.fromConn}:${w.fromPin} → ${w.toConn}:${w.toPin})?`, body: <>This will remove the wire from the harness. The connection between {w.fromConn}:{w.fromPin} and {w.toConn}:{w.toPin} will be lost., onConfirm: async () => { if (!w._raw || !w._raw.id) { toast.err('No backend id for this wire'); return; } try { await kfxDeleteWire(w._raw.id); setData(d => d.filter(x => x.id !== w.id)); toast.ok(`Deleted wire ${w.fromConn}:${w.fromPin} → ${w.toConn}:${w.toPin}`); if (onRefresh) onRefresh(); } catch (e) { toast.err('Could not delete wire', (e && e.message) || 'Server error'); } } }); }; const createWire = async (draft) => { if (!realPid) throw new Error('No project loaded'); await kfxCreateWire(realPid, draft); toast.ok('Wire added', `${draft.fromConn}:${draft.fromPin} → ${draft.toConn}:${draft.toPin}`); setShowCreate(false); if (onRefresh) await onRefresh(); }; const filtered = data.filter(w => { const q = filter.trim().toLowerCase(); const ok = !q || [w.net, w.color, w.gauge, w.fromConn, w.toConn, w.notes, w.cable].some(v => (v + '').toLowerCase().includes(q)); const co = !filterColor || w.color === filterColor; const sh = !filterShield || w.shield; return ok && co && sh; }); if (loading && data.length === 0) { return (
Loading wires…
); } return (
Filter Sort Import CSV setShowCreate(true)}>New wire }/> {/* Filter bar */}
setFilter(e.target.value)} aria-label="Filter wires"/> {filter && } /
setFilterShield(!filterShield)} icon="bolt">Shielded only
{filtered.length} / {data.length} wires · {filtered.filter(w => w.shield).length} shielded · {filtered.filter(w => w.twisted).length} twisted
{/* Table */}
{filtered.length === 0 ? ( setShowCreate(true)}>Add wire} secondary={!filter && Import CSV} /> ) : ( {filtered.map(w => { const isOpen = (w.notes || '').toLowerCase().includes('open'); return ( setSelRow(w.id)}> ); })}
Cav Status From To Color Gauge Spec Length (mm) Cable Net Notes
{w.cavity} {isOpen ? ( Open ) : ( Routed )} {w.fromConn}:{w.fromPin} {w.toConn}:{w.toPin} {w.shield && } {w.twisted && } {w.cable} {w.net}
del(w)}/>
)}
{/* Confirm modal */} {confirm && ( setConfirm(null)}/> )} {/* Create modal */} {showCreate && ( setShowCreate(false)} onCreate={createWire}/> )}
); }; /* ---------- Create wire modal — controlled form, calls backend on submit ---------- */ const CreateWireModal = ({ connectors=[], onCreate, onCancel }) => { const [draft, setDraft] = useState({ fromConn: connectors[0]?.id || '', fromPin: '1', toConn: connectors[1]?.id || connectors[0]?.id || '', toPin: '1', color: 'Black', gauge: '22 AWG', spec: '', length: 500, cable: 'Single core', net: '', shield: false, twisted: false, notes: '', }); const setF = (k, v) => setDraft(d => ({ ...d, [k]: v })); const canSubmit = !!(draft.fromConn && draft.toConn && draft.fromPin !== '' && draft.toPin !== ''); return ( Cancel { if (canSubmit) await onCreate(draft); }}> Create wire }>
e.preventDefault()}>
setF('fromPin', e.target.value)}/>
setF('toPin', e.target.value)}/>
setF('net', e.target.value)}/>
setF('length', +e.target.value || 0)}/>
setF('notes', e.target.value)}/>
setF('shield', e.target.checked)}/>
setF('twisted', e.target.checked)}/>
); }; /* ---------- Shared view header ---------- */ const ViewHeader = ({ title, hint, count, countTotal, actions, badge }) => (

{title}

{badge} {count !== undefined && ( {count}{countTotal !== undefined && count !== countTotal && / {countTotal}} )}
{hint &&
{hint}
}
{actions}
); const STYLE = ` .kf-view { display: flex; flex-direction: column; height: 100%; min-height: 0; } .kf-view-head { display: flex; align-items: center; gap: 14px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); background: var(--bg-1); flex-wrap: wrap; } .kf-view-head-l { display: flex; align-items: baseline; gap: 12px; min-width: 0; } .kf-view-head-l h1 { font-family: var(--f-display); font-size: 20px; font-weight: 700; letter-spacing: -.3px; } .kf-view-count { font-size: 13px; color: var(--t-2); font-weight: 500; } .kf-view-hint { font-size: 12px; color: var(--t-3); } .kf-view-head-r { margin-left: auto; display: flex; gap: 6px; } .kf-toolbar { display: flex; align-items: center; gap: 10px; padding: 10px 18px; background: var(--bg-2); border-bottom: 1px solid var(--line); font-size: 12px; } .kf-toolbar-info { color: var(--t-3); font-size: 11px; } .kf-search { position: relative; display: flex; align-items: center; gap: 7px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--r-2); padding: 0 8px 0 10px; min-width: 320px; height: 32px; } .kf-search input { flex: 1; background: transparent; border: none; outline: none; color: var(--t-1); font-size: 12.5px; font-family: var(--f-sans); } .kf-search input::placeholder { color: var(--t-4); } .kf-search-clear { background: transparent; border: none; color: var(--t-3); cursor: pointer; display: flex; align-items: center; padding: 4px; border-radius: 3px; } .kf-search-clear:hover { background: var(--bg-3); color: var(--t-1); } .kf-search-kbd { display: flex; align-items: center; padding-left: 4px; border-left: 1px solid var(--line); margin-left: 2px; } .kf-select { height: 32px; background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--r-2); color: var(--t-1); font-size: 12px; padding: 0 8px; font-family: var(--f-sans); } /* Table */ .kf-table-wrap { flex: 1; overflow: auto; } .kf-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12px; font-variant-numeric: tabular-nums; } .kf-table th { position: sticky; top: 0; z-index: 2; background: var(--bg-2); color: var(--t-3); font-size: 9.5px; font-weight: 700; letter-spacing: .8px; text-transform: uppercase; text-align: left; padding: 9px 10px; border-bottom: 1px solid var(--line-strong); white-space: nowrap; } .kf-table th.num { text-align: right; } .kf-table th.kf-th-sticky { position: sticky; left: 0; z-index: 3; background: var(--bg-2); } .kf-table td { padding: 7px 10px; height: var(--density-row); border-bottom: 1px solid var(--line); vertical-align: middle; color: var(--t-1); white-space: nowrap; } .kf-table td.kf-td-sticky { position: sticky; left: 0; background: var(--bg-1); color: var(--t-3); font-weight: 600; } .kf-table tr:hover td { background: rgba(74,158,255,.05); } .kf-table tr:hover td.kf-td-sticky { background: rgba(74,158,255,.08); } .kf-table tr.sel td { background: rgba(74,158,255,.12) !important; } .kf-table td.mono { font-family: var(--f-mono); } .kf-table td b { color: var(--t-1); font-weight: 600; } .kf-color-cell { display: inline-flex; align-items: center; gap: 8px; } .kf-color-sw { width: 22px; height: 10px; border-radius: 2px; border: 1px solid rgba(255,255,255,.1); } .kf-color-code { font-family: var(--f-mono); font-size: 11px; font-weight: 700; color: var(--t-2); min-width: 18px; } .kf-color-name { color: var(--t-2); } .kf-cable-mini { color: var(--t-2); display: inline-flex; align-items: center; gap: 6px; } .kf-cable-dot { font-family: var(--f-mono); font-size: 14px; line-height: 1; } .kf-row-actions { display: flex; gap: 2px; opacity: .35; transition: opacity .15s; } .kf-table tr:hover .kf-row-actions { opacity: 1; } /* Form */ .kf-form { display: flex; flex-direction: column; gap: 14px; } .kf-form-row { display: flex; gap: 12px; } .kf-field { flex: 1; display: flex; flex-direction: column; gap: 5px; min-width: 0; } .kf-field label { font-size: 10px; text-transform: uppercase; letter-spacing: .6px; font-weight: 700; color: var(--t-3); } .kf-input { background: var(--bg-1); border: 1px solid var(--line); border-radius: var(--r-2); color: var(--t-1); font-size: 13px; padding: 8px 10px; font-family: var(--f-sans); height: 36px; width: 100%; min-width: 0; box-sizing: border-box; } textarea.kf-input { height: auto; min-height: 60px; resize: vertical; } .kf-form-row { display: flex; gap: 12px; flex-wrap: wrap; } .kf-form-row > .kf-field { min-width: 200px; } .kf-field-hint { font-size: 11.5px; color: var(--t-3); line-height: 1.4; margin-top: 2px; } .kf-card-sub { font-size: 12px; color: var(--t-3); margin-left: 8px; font-weight: 400; } .kf-input:focus { border-color: var(--info); box-shadow: 0 0 0 3px var(--info-soft); outline: none; } .kf-checkfield { display: flex; align-items: center; gap: 8px; } .kf-checkfield input { width: 16px; height: 16px; accent-color: var(--info); } .kf-checkfield label { font-size: 13px; color: var(--t-1); cursor: pointer; } `; document.head.appendChild(Object.assign(document.createElement('style'), { textContent: STYLE })); window.WiresView = WiresView; window.ViewHeader = ViewHeader;