diff

<tosi-diff> renders a unified, line-by-line diff between two strings — the kind of view you'd see in a code review, with removed lines marked - and added lines marked +.

Set its original and modified properties (or attributes) to the two versions of the text; it recomputes and re-renders. (They're named original/modified rather than before/after to avoid the native Element.before()/after() methods.)

const { tosiDiff } = tosijsui

preview.append(
  tosiDiff({
    original: 'one\ntwo\nthree\nfour',
    modified: 'one\nTWO\nthree\nfour\nfive',
    style: { width: '100%', height: '100%' },
  })
)

The diff is computed with a longest-common-subsequence pass over the lines, so unchanged lines are shown as context and only genuine insertions/deletions are highlighted. The pure diffLines(before, after) function is exported too, if you just want the data.

Resolving a diff — "do I want to accept this?"

Set resolvable and each change gets a pair of buttons: keep the original, or take the modified. value is the text those choices imply, and a change event fires whenever one moves. That makes it a review surface for a proposed edit — an AI suggestion, a collaborator's revision, a file that moved under you while you were editing it.

The labels are yours. "Mine"/"Theirs" is git's framing, "Current"/"Proposed" suits reviewing a suggestion, and the component's own vocabulary is original/modified — so nothing is hardcoded. Pass already-localized strings; the host owns that choice.

const { tosiDiff } = tosijsui

const diff = tosiDiff({
  original: 'The cat sat on the mat.\nIt was a sunny day.\nThe end.',
  modified: 'The cat sprawled across the mat.\nIt was a sunny day.\nFin.',
  resolvable: true,
  originalLabel: 'Mine',
  modifiedLabel: 'Theirs',
  style: { width: '100%' },
})
diff.addEventListener('change', () => {
  result.textContent = JSON.stringify(diff.value)
})
const result = document.createElement('pre')
result.textContent = JSON.stringify(diff.value)
preview.append(diff, result)
const { tosiDiff } = tosijsui
const el = tosiDiff({
  original: 'a\nKEEP\nc\nDROP\ne',
  modified: 'a\nNEW1\nc\nNEW2\ne',
  resolvable: true,
})
document.body.append(el)

test('accepting everything yields the modified text', () => {
  expect(el.changeCount).toBe(2)
  expect(el.value).toBe('a\nNEW1\nc\nNEW2\ne')
})

test('rejecting everything yields the original text exactly', () => {
  el.rejectAll()
  expect(el.value).toBe('a\nKEEP\nc\nDROP\ne')
  el.remove()
})

The unit of decision is a change block, not a line: a multi-line edit is one choice, because accepting half of one produces text neither side wrote. Two more exports give you the same model without the DOM — diffBlocks(diffLines(a, b)) for the blocks, and resolveDiff(blocks, choices) to turn choices back into text.

acceptAll() and rejectAll() do the obvious thing, changeCount tells you how many decisions the diff is asking for (0 means the texts agree), and resolutions is readable and writable if you want to drive it yourself. Resolutions reset when either text changes — decisions belong to the diff they were made about.