/** Where removed rows go back when their Undo comes LATER than the removal. * * A raw index recorded at removal time drifts: an add or a reorder landing in between (another tab, * the rail, the player) shifts everything after it, and the row came back one or more places off. * So each removed row goes back right after the row that preceded it in the list * it was removed from — its neighbour, looked up in the list as it stands NOW — or at the start when * it was first or no earlier neighbour is left. Rows already back in the list are skipped (the * server's add is idempotent and would leave them where they are). * * @param before the list's ids when the rows were removed, in stored order * @param removed the ids taken out * @param now the list's ids at Undo time, in stored order * @returns the ids to add back and, paired one to one, the index each lands at — ascending, the * shape the server's insert takes (each index counts the rows put back before it). */ export function reinsertAt( before: readonly string[], removed: readonly string[], now: readonly string[], ): { keys: string[]; positions: number[] } { const out = new Set(removed); const list = [...now]; const placed: string[] = []; for (let i = 0; i < before.length; i++) { const id = before[i]!; if (!out.has(id) || now.includes(id)) continue; let at = 0; for (let j = i - 1; j >= 0; j--) { const k = list.indexOf(before[j]!); if (k >= 0) { at = k + 1; break; } } list.splice(at, 0, id); placed.push(id); } const keys = placed.map((id) => ({ id, at: list.indexOf(id) })).sort((a, b) => a.at - b.at); return { keys: keys.map((k) => k.id), positions: keys.map((k) => k.at) }; }