Skip to content

Diff viewer component

components specs/components/diff-viewer.kmd

The component that renders a source-code diff in a code-hosting surface: split (side-by-side) and unified (inline) modes, per-file sticky header with collapse + stats, expandable context hunks, line-level add/remove/ context styling, and anchors for inline review comments. Modeled after the GitHub diff view (Primer). Used by Koder Flow PR/commit/compare surfaces.

When this spec applies

Primary triggers

All triggers

Specification body

Component — Diff viewer

Status: v0.1.0 — Draft. Promoted from the GitHub Primer parity scan (meta/docs/stack #092). Live URL once rendered: kds.koder.dev/<locale>/components/components-diff-viewer.html. Source: https://primer.style/ (diff / Blob view patterns).

R1 — Two modes

  • Unified (inline): removed and added lines interleaved in one column; default on narrow viewports.
  • Split (side-by-side): old on the left, new on the right; default on wide viewports.
  • The mode is a user toggle, persisted per user (not per file). Both modes render identical semantic line data (R3) — only layout differs.

R2 — Per-file block

Each changed file is its own block with a sticky header (stays pinned while scrolling that file's hunks) containing:

  • file path (old→new on rename), with directory de-emphasized;
  • change stats (+a −b) using the add/remove semantic colors;
  • a collapse/expand toggle (collapsed files render header only);
  • file-level status (added / modified / renamed / deleted / binary).

R3 — Line model

Every rendered line is exactly one of: context, added, removed, or hunk-header (the @@ … @@ separator). Add/remove use semantic fills + a leading gutter marker (+/) so the distinction never relies on color alone (R7). Old and new line numbers render in gutters (both columns in split; two gutters in unified). Syntax highlighting is delegated to specs/components/code-block.kmd (the diff layer only adds the change overlay).

R4 — Expandable context

Between hunks, an expand-context control loads the hidden unchanged lines (expand up / down / all). Expansion fetches incrementally; the expanded lines render as context. Reaching file start/end disables the respective direction.

R5 — Inline comment anchors

  • Any line exposes an affordance (on hover / keyboard focus) to start a review comment thread anchored to that line (and, for multi-line selection, a line range).
  • Anchored threads render inline beneath the line in both modes and are owned by specs/patterns/code-review.kmd (this component provides the anchor + slot, not the thread UI).
  • Anchors are stable to context expansion (anchored by line identity, not scroll position).

R6 — Large & non-text diffs

  • Files over a size/line threshold render collapsed by default with a "Load diff" action (avoid freezing on huge diffs); the threshold is a surface setting, and truncation is always disclosed (no silent cap).
  • Binary files show a "Binary file changed" notice; images show an old/new visual compare (side-by-side / swipe), not a text diff.

R7 — Accessibility

  • Add/remove conveyed by gutter marker + line-number column + semantic fill (never color alone — WCAG 1.4.1).
  • The diff is navigable by keyboard (next/prev changed hunk, next comment thread); line numbers are programmatically associated with their lines.
  • Screen-reader output announces line kind ("added line 42: …").

R8 — Tokens & i18n

  • All fills/markers from Verge tokens (specs/themes/verge.kmd); light/ dark via the token layer.
  • Chrome strings (stats label, "Load diff", "Expand", file status, "Binary file changed") are translatable per specs/i18n/contract.kmd; code content itself is never translated.

Não-escopo

  • Syntax highlighting internals (specs/components/code-block.kmd).
  • The review thread/decision UI (specs/patterns/code-review.kmd).
  • Diff computation / server-side hunking (Koder Flow backend).
  • Word-level intra-line diff (optional v1 enhancement).

Requirements (testable)

Requirement: Split and unified modes with persisted toggle {#req-diff-viewer-modes}

The diff viewer SHALL offer both a unified (inline) and a split (side-by-side) mode selectable by a user toggle, SHALL persist the chosen mode per user rather than per file, and SHALL render identical semantic line data in both modes so that only the layout differs.

Scenario: User toggles to split mode

  • GIVEN a rendered diff currently displayed in unified mode
  • WHEN the user activates the mode toggle to split
  • THEN the removed lines render in a left column and the added lines render in a right column
  • AND the set of semantic lines shown (context/added/removed/hunk-header) is unchanged from unified mode

Scenario: Mode choice persists across files and reloads

  • GIVEN a user who has selected split mode
  • WHEN the user opens a different file's diff and reloads the surface
  • THEN the diff renders in split mode
  • AND the persisted preference is the same value regardless of which file is viewed

Scenario: Narrow viewport defaults to unified

  • GIVEN a user with no previously persisted mode preference
  • WHEN the diff is rendered on a narrow viewport
  • THEN the default mode is unified

Requirement: Per-file block with sticky header {#req-diff-viewer-per-file-block}

Each changed file SHALL be rendered as its own block with a sticky header that stays pinned while its hunks scroll, and that header SHALL display the file path, the change stats (+a −b), a collapse/expand toggle, and the file-level status.

Scenario: Header stays pinned while scrolling hunks

  • GIVEN a file block whose hunks extend beyond the viewport
  • WHEN the user scrolls within that file's hunks
  • THEN the file header remains pinned at the top of the block showing path, stats, and status
  • AND the collapse/expand toggle remains reachable

Scenario: Collapsed file renders header only

  • GIVEN an expanded file block
  • WHEN the user activates the collapse toggle
  • THEN only the file header is rendered
  • AND none of the file's diff lines are present in the output

Requirement: Line model with non-color change encoding {#req-diff-viewer-line-model}

Every rendered line SHALL be exactly one of context, added, removed, or hunk-header, and each added or removed line SHALL carry a leading gutter marker (+/) together with old/new line numbers in the gutters so the added/removed distinction never relies on color alone.

Scenario: Added line carries marker and number gutters

  • GIVEN a diff containing an added line
  • WHEN the line is rendered
  • THEN it is classified as added
  • AND it shows a leading + gutter marker
  • AND its new line number is present in the line-number gutter

Scenario: Every line has exactly one kind

  • GIVEN a rendered diff with context, changes, and hunk separators
  • WHEN each rendered line is inspected
  • THEN each line is assigned exactly one of context, added, removed, or hunk-header

Requirement: Expandable context between hunks {#req-diff-viewer-expandable-context}

Between hunks the viewer SHALL provide an expand-context control that loads the hidden unchanged lines as context, and it SHALL disable the up or down direction when the file's start or end has been reached.

Scenario: Expanding reveals hidden context lines

  • GIVEN two hunks separated by hidden unchanged lines
  • WHEN the user activates the expand-context control
  • THEN the previously hidden lines are rendered as context
  • AND they appear between the two hunks

Scenario: Direction disabled at file boundary

  • GIVEN an expand-context control adjacent to the first line of the file
  • WHEN context has been expanded up to the file start
  • THEN the expand-up direction is disabled

Requirement: Keyboard navigation and screen-reader announcement {#req-diff-viewer-a11y}

The diff SHALL be navigable by keyboard to move between changed hunks and comment threads, line numbers SHALL be programmatically associated with their lines, and screen-reader output SHALL announce each line's kind (for example "added line 42").

Scenario: Keyboard moves to next changed hunk

  • GIVEN a diff with multiple changed hunks and keyboard focus on the viewer
  • WHEN the user invokes the next-changed-hunk key
  • THEN focus moves to the following changed hunk

Scenario: Screen reader announces line kind

  • GIVEN a screen reader focused on an added line at line number 42
  • WHEN the line receives focus
  • THEN the announced output includes the line kind "added" and the line number 42

References