// mod-table.jsx - BranchPicker + ModTable components
// Loaded after sortof-shared.jsx; defines globals that sortof-app.jsx references.
const { useState, useMemo } = React;
function BranchPicker({ wsid, branches, selected, userTouched, onToggle }) {
const radio = isRadioMode(branches);
const inputType = radio ? 'radio' : 'checkbox';
const activeSet = new Set(D.SORTED_ORDER || []);
const effective = userTouched ? selected : defaultSelectionForBranches(branches, activeSet);
return (
workshop {wsid} · {branches.length} mod_ids · {radio ? 'pick one' : 'multi-select'}
{branches.map(b => {
const checked = effective.includes(b.modId);
return (
);
})}
|
);
}
function ModTable({ defaultOpen = false, branchSelections, onToggleBranch, expandedWsids, onToggleExpansion }) {
const [open, setOpen] = useState(defaultOpen);
const groupedByWsid = useMemo(() => {
const g = {};
for (const m of (D.MOD_DB || [])) {
const w = m.wsid || '';
if (!w) continue;
(g[w] = g[w] || []).push(m);
}
return g;
}, [D.MOD_DB]);
const rowSpecs = useMemo(() => {
const specs = [];
const seenWsid = new Set();
for (const modId of (D.SORTED_ORDER || [])) {
const m = (D.MOD_DB || []).find(x => x.modId === modId);
if (!m || !m.wsid) continue;
if (seenWsid.has(m.wsid)) continue;
seenWsid.add(m.wsid);
const branches = groupedByWsid[m.wsid] || [m];
const isMulti = branches.length >= 2;
specs.push({ wsid: m.wsid, primary: m, branches, isMulti });
}
for (const w of Object.keys(groupedByWsid)) {
if (!seenWsid.has(w)) {
const branches = groupedByWsid[w];
specs.push({ wsid: w, primary: branches[0], branches, isMulti: branches.length >= 2 });
}
}
return specs;
}, [D.SORTED_ORDER, D.MOD_DB, groupedByWsid]);
return (
setOpen(!open)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setOpen(!open); } }}>
mod details
· {(D.SORTED_ORDER || []).length} mods
why everything ended up where it did
{open ? '▾' : '▸'}
{open && (
| # |
mod id |
workshop id |
category |
dependencies |
load |
{rowSpecs.map((spec, i) => {
const idx = String(i + 1).padStart(2, '0');
if (!spec.isMulti) {
const m = spec.primary;
const rowState = rowStateForMod(m.modId);
const stateGlyph = { resolved: '✓', warning: '⚠', error: '✗' }[rowState] || '·';
return (
|
{stateGlyph}{idx}
|
{m.modId} |
{spec.wsid} |
{m.cat} |
{m.deps && m.deps.length ? m.deps.join(', ') : '-'} |
{m.pos === 'first' && first}
{m.pos === 'last' && last}
{!m.pos && -}
|
);
}
const selected = branchSelections[spec.wsid] || [];
const N = spec.branches.length;
const X = selected.length;
const userTouched = (spec.wsid in branchSelections);
const affordance = userTouched ? `✓ ${X} of ${N}` : `▾ ${N} branches`;
const firstSelected = spec.branches.find(b => selected.includes(b.modId)) || spec.branches[0];
const showAsZero = userTouched && X === 0;
const display = showAsZero ? null : firstSelected;
const expanded = expandedWsids.has(spec.wsid);
const rowState = display ? rowStateForMod(display.modId) : 'resolved';
const stateGlyph = { resolved: '✓', warning: '⚠', error: '✗' }[rowState] || '·';
return (
|
{stateGlyph}{idx}
|
|
{spec.wsid} |
{display ? {display.cat} : '-'} |
{display && display.deps && display.deps.length ? display.deps.join(', ') : '-'} |
{display && display.pos === 'first' && first}
{display && display.pos === 'last' && last}
{(!display || !display.pos) && -}
|
{expanded && (
)}
);
})}
)}
);
}
Object.assign(window, { ModTable, BranchPicker, rowStateForMod });