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
- Build a diff view for a code-hosting surface
All triggers
- Render a code diff with added/removed lines
- Show a pull request or commit's file changes
- Let a reviewer comment on a specific diff line
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, orhunk-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
specs/components/code-block.kmdspecs/patterns/code-review.kmdspecs/themes/verge.kmdspecs/i18n/contract.kmd