Diff
Summary
A line-based change view: each .soma-diff-line is a
-gutter (line number) plus -text
(whitespace preserved), with -add/-del
tinting and a -hunk header row. Keep the
+/- markers in the text: the color
reinforces the change, it never carries it alone. This is the
substrate for plan-diff interactions ("this will change X —
approve?"); gutters use muted, not subtle, text.
Markup contract (axe-gated): long lines scroll horizontally, so
the block must be keyboard-reachable: it carries
tabindex="0" and an aria-label.
When to use
| Case | Use it for |
|---|---|
| Plan diffs | What an action will change before it runs, paired with approve/reject controls (the Cortex PlanDiff composition; see Cortex). |
| Config drift | Current vs desired state, surfacing what reconciliation would touch. |
| Audit detail | What a past change actually did, line by line. |
| Not a review UI | No side-by-side, syntax highlighting or comment threads; that's an app-level tool, not this component. |
Examples
HTML
The pane and the four line kinds: context (unmarked),
-del, -add, and a -hunk
header. tabindex="0" and aria-label
are required, not decoration: without the tabindex, keyboard
users cannot scroll long lines into view.
<div class="soma-diff" tabindex="0" aria-label="Plan diff">
<div class="soma-diff-line soma-diff-hunk"><span class="soma-diff-gutter"></span>
<span class="soma-diff-text">@@ service "api-backend" @@</span></div>
<div class="soma-diff-line"><span class="soma-diff-gutter">12</span>
<span class="soma-diff-text"> memory = 512</span></div>
<div class="soma-diff-line soma-diff-del"><span class="soma-diff-gutter">13</span>
<span class="soma-diff-text">- replicas = 3</span></div>
<div class="soma-diff-line soma-diff-add"><span class="soma-diff-gutter">13</span>
<span class="soma-diff-text">+ replicas = 6</span></div>
</div>
Whitespace in -text is preserved
(white-space: pre): indentation and alignment
survive exactly as emitted, which is why the context lines above
carry two leading spaces to stay aligned with the
+/- lines:
<span class="soma-diff-text"> memory = 512</span> <!-- two spaces kept -->
A hunk header names the section that follows; its gutter span is present but empty (the columns stay aligned):
<div class="soma-diff-line soma-diff-hunk">
<span class="soma-diff-gutter"></span>
<span class="soma-diff-text">@@ autoscaler "api-backend" @@</span>
</div>
CSS classes
| Class | Effect |
|---|---|
.soma-diff | The block: monospace, bordered, inset surface (--soma-surface-inset), scrolls horizontally on long lines; shows the focus ring when focused. |
.soma-diff-line | One line: gutter + text. |
.soma-diff-add / .soma-diff-del | Success/danger subtle background with matching -subtle-fg text (gutter included); keep the +/- in the text. |
.soma-diff-hunk | Section header row (info tint); leave its gutter empty. |
.soma-diff-gutter | Line number: muted text (informational, AA-checked), end-aligned, 52px, non-selectable so copied diffs grab only the text. |
.soma-diff-text | The line content; whitespace is preserved (pre). |
Rendering from data
Producing the diff is the consumer's concern — a plan endpoint
usually returns it as structured lines. Render with
textContent (diff payloads quote user data; never
innerHTML them), map the kind to the line class,
and leave hunk gutters empty:
const KIND_CLASS = { add: 'soma-diff-add', del: 'soma-diff-del', hunk: 'soma-diff-hunk' };
function renderDiff(pane, lines) {
// lines: [{kind: 'hunk'|'add'|'del'|'context', no?, text}]
pane.replaceChildren(...lines.map(({ kind, no, text }) => {
const line = document.createElement('div');
line.className = 'soma-diff-line';
if (KIND_CLASS[kind]) line.classList.add(KIND_CLASS[kind]);
const gutter = document.createElement('span');
gutter.className = 'soma-diff-gutter';
gutter.textContent = kind === 'hunk' ? '' : String(no);
const body = document.createElement('span');
body.className = 'soma-diff-text';
body.textContent = text; // '+ replicas = 6' — marker included
line.append(gutter, body);
return line;
}));
}
const plan = await fetch('/api/plans/4821/diff').then((r) => r.json());
renderDiff(document.querySelector('#plan-diff'), plan.lines);
The plan-diff composition
The Cortex ApprovalCard wraps this component: a
widget whose body holds the diff pane
and whose footer is a .soma-form-actions row of
approve/reject buttons — the diff
itself stays inert. The pane's aria-label should
name the plan ("Plan diff for deploy #4821"), because that label
is what a screen-reader user hears before deciding:
<section class="soma-widget">
<header class="soma-widget-header">
<h3 class="soma-widget-title">Scale api-backend to 6 replicas</h3>
</header>
<div class="soma-widget-body">
<div class="soma-diff" id="plan-diff" tabindex="0"
aria-label="Plan diff for deploy #4821"></div>
<div class="soma-form-actions">
<button class="soma-button soma-button-primary">Approve</button>
<button class="soma-button">Reject</button>
</div>
</div>
</section>
See the Nware Cortex kit for the full trust-primitive mapping (PlanDiff, ApprovalCard and friends).
JavaScript
None in Soma — the diff is CSS-only. The renderer above is the consumer's wiring pattern; any approve/reject controls around it are ordinary buttons.