/* ===================================================================
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 (
);
}
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}
/>
) : (
| Cav |
Status |
From |
To |
Color |
Gauge |
Spec |
Length (mm) |
Cable |
Net |
Notes |
|
{filtered.map(w => {
const isOpen = (w.notes || '').toLowerCase().includes('open');
return (
setSelRow(w.id)}>
| {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
>
}>
);
};
/* ---------- 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;