@ashley-shrok/viewmodel-shell 6.4.0 → 6.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.d.ts +13 -0
- package/dist/browser.js +116 -3
- package/dist/index.d.ts +71 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/styles/default.css +106 -0
package/dist/browser.d.ts
CHANGED
|
@@ -250,4 +250,17 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
250
250
|
* with an `action` becomes a role="button" tabstop with Enter/Space activation
|
|
251
251
|
* (Space suppresses page scroll), mirroring TableRow.action / SectionNode.action. */
|
|
252
252
|
private tracker;
|
|
253
|
+
/** DiffNode — aligned before/after primitive. Row-kind (add/remove/context)
|
|
254
|
+
* is derived client-side from the shape of DiffRow: old-only = remove,
|
|
255
|
+
* new-only = add, matching text = context, differing text = modified pair
|
|
256
|
+
* (side-by-side shows both cells; unified splits into remove-then-add).
|
|
257
|
+
* Line-number cells carry the same kind class as their content cell so the
|
|
258
|
+
* whole row-side reads as one connected colored band. Left-stripe lives only
|
|
259
|
+
* on the leftmost cell of each colored row-side (linenum cell). In unified
|
|
260
|
+
* mode the two linenum columns visually collapse into a single left margin
|
|
261
|
+
* for context rows; for add/remove rows the second linenum keeps its tint so
|
|
262
|
+
* the color band runs continuously, minus the stripe. Design of record:
|
|
263
|
+
* `.planning/design/diff-node.md`. Spike-validated 2026-07-19. */
|
|
264
|
+
private diff;
|
|
265
|
+
private _diffCell;
|
|
253
266
|
}
|
package/dist/browser.js
CHANGED
|
@@ -509,6 +509,7 @@ export class BrowserAdapter {
|
|
|
509
509
|
case "breadcrumb": return this.breadcrumb(n, parent, on);
|
|
510
510
|
case "steps": return this.steps(n, parent);
|
|
511
511
|
case "tracker": return this.tracker(n, parent, on);
|
|
512
|
+
case "diff": return this.diff(n, parent);
|
|
512
513
|
default: {
|
|
513
514
|
// Fail loud, not silent (AGENTS.md: "Nothing important fails quietly").
|
|
514
515
|
// Runtime trees are server-controlled JSON, so an unknown/forward-version
|
|
@@ -3033,9 +3034,13 @@ export class BrowserAdapter {
|
|
|
3033
3034
|
// filter-narrow that equals all matches; under pagination it is the current
|
|
3034
3035
|
// page (selection accumulates via the round-tripped binds); over-cap /
|
|
3035
3036
|
// zero-match render no rows, so the control below simply is not drawn and
|
|
3036
|
-
// can never claim to select rows that are not on screen.
|
|
3037
|
-
//
|
|
3038
|
-
//
|
|
3037
|
+
// can never claim to select rows that are not on screen. It writes the binds
|
|
3038
|
+
// and — for checkboxes that carry an `action` — replays each one's dispatch,
|
|
3039
|
+
// so select-all does EXACTLY what clicking each row by hand does (see the
|
|
3040
|
+
// change handler below). It is NOT the removed 0.15.0 selection.action seam:
|
|
3041
|
+
// that was the framework's own per-toggle seam racing with no coalescing loop;
|
|
3042
|
+
// this reuses CheckboxNode.action (a shipped, app-declared capability) and the
|
|
3043
|
+
// v4.2 non-blocking latest-wins loop, and never forces a dispatch semantic.
|
|
3039
3044
|
const rowCheckboxes = n.rows.flatMap(r => (r.actions ?? []).filter((e) => e.type === "checkbox"));
|
|
3040
3045
|
if (rowCheckboxes.length > 0) {
|
|
3041
3046
|
const lbl = document.createElement("label");
|
|
@@ -3059,6 +3064,22 @@ export class BrowserAdapter {
|
|
|
3059
3064
|
rowInp.checked = target;
|
|
3060
3065
|
}
|
|
3061
3066
|
inp.indeterminate = false;
|
|
3067
|
+
// Select-all is N per-row toggles expressed at once: after writing every
|
|
3068
|
+
// bind, replay each checkbox's OWN dispatch. Behaviorally identical to the
|
|
3069
|
+
// user clicking each row by hand — same actions, same order, same blocking/
|
|
3070
|
+
// coalescing — so it introduces ZERO new dispatch semantics. A checkbox with
|
|
3071
|
+
// no `action` dispatches nothing, so the pure client-harvest model (selection
|
|
3072
|
+
// stays client-side until a bulk-button harvest) is untouched. The app owns
|
|
3073
|
+
// what a burst means: the non-blocking latest-wins loop it opted into
|
|
3074
|
+
// collapses N identical dispatches to one server recompute; a blocking
|
|
3075
|
+
// checkbox serializes under the dispatch guard exactly as rapid manual
|
|
3076
|
+
// clicking would. (Consumer-driven — Poppy/PBMInvoices 2026-07-20: a
|
|
3077
|
+
// server-tracked SelectedMap went stale on select-all because only per-row
|
|
3078
|
+
// toggles dispatched, not the header box.)
|
|
3079
|
+
for (const cb of rowCheckboxes) {
|
|
3080
|
+
if (cb.action)
|
|
3081
|
+
on(cb.action);
|
|
3082
|
+
}
|
|
3062
3083
|
});
|
|
3063
3084
|
lbl.appendChild(inp);
|
|
3064
3085
|
lbl.appendChild(mark);
|
|
@@ -3608,4 +3629,96 @@ export class BrowserAdapter {
|
|
|
3608
3629
|
}
|
|
3609
3630
|
parent.appendChild(strip);
|
|
3610
3631
|
}
|
|
3632
|
+
/** DiffNode — aligned before/after primitive. Row-kind (add/remove/context)
|
|
3633
|
+
* is derived client-side from the shape of DiffRow: old-only = remove,
|
|
3634
|
+
* new-only = add, matching text = context, differing text = modified pair
|
|
3635
|
+
* (side-by-side shows both cells; unified splits into remove-then-add).
|
|
3636
|
+
* Line-number cells carry the same kind class as their content cell so the
|
|
3637
|
+
* whole row-side reads as one connected colored band. Left-stripe lives only
|
|
3638
|
+
* on the leftmost cell of each colored row-side (linenum cell). In unified
|
|
3639
|
+
* mode the two linenum columns visually collapse into a single left margin
|
|
3640
|
+
* for context rows; for add/remove rows the second linenum keeps its tint so
|
|
3641
|
+
* the color band runs continuously, minus the stripe. Design of record:
|
|
3642
|
+
* `.planning/design/diff-node.md`. Spike-validated 2026-07-19. */
|
|
3643
|
+
diff(n, parent) {
|
|
3644
|
+
const mode = n.mode ?? "side-by-side";
|
|
3645
|
+
const root = document.createElement("div");
|
|
3646
|
+
root.className = `vms-diff vms-diff--${mode}`;
|
|
3647
|
+
if (n.id != null)
|
|
3648
|
+
root.id = n.id;
|
|
3649
|
+
root.setAttribute("role", "group");
|
|
3650
|
+
root.setAttribute("aria-label", "Diff");
|
|
3651
|
+
// Optional header row(s) — file paths for old / new.
|
|
3652
|
+
if (n.header) {
|
|
3653
|
+
if (mode === "side-by-side") {
|
|
3654
|
+
const hOld = document.createElement("div");
|
|
3655
|
+
hOld.className = "vms-diff__header vms-diff__header--old";
|
|
3656
|
+
hOld.textContent = n.header.old;
|
|
3657
|
+
root.appendChild(hOld);
|
|
3658
|
+
const hNew = document.createElement("div");
|
|
3659
|
+
hNew.className = "vms-diff__header vms-diff__header--new";
|
|
3660
|
+
hNew.textContent = n.header.new;
|
|
3661
|
+
root.appendChild(hNew);
|
|
3662
|
+
}
|
|
3663
|
+
else {
|
|
3664
|
+
const h = document.createElement("div");
|
|
3665
|
+
h.className = "vms-diff__header";
|
|
3666
|
+
h.textContent = `${n.header.old} → ${n.header.new}`;
|
|
3667
|
+
root.appendChild(h);
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
for (const row of n.rows) {
|
|
3671
|
+
const oldCell = row.old ?? null;
|
|
3672
|
+
const newCell = row.new ?? null;
|
|
3673
|
+
const kindOld = oldCell == null ? "empty"
|
|
3674
|
+
: newCell == null ? "remove"
|
|
3675
|
+
: oldCell.text === newCell.text ? "context"
|
|
3676
|
+
: "remove";
|
|
3677
|
+
const kindNew = newCell == null ? "empty"
|
|
3678
|
+
: oldCell == null ? "add"
|
|
3679
|
+
: newCell.text === oldCell.text ? "context"
|
|
3680
|
+
: "add";
|
|
3681
|
+
if (mode === "side-by-side") {
|
|
3682
|
+
this._diffCell(root, oldCell?.lineNumber, kindOld, true, "old");
|
|
3683
|
+
this._diffCell(root, oldCell?.text, kindOld, false, "old");
|
|
3684
|
+
this._diffCell(root, newCell?.lineNumber, kindNew, true, "new");
|
|
3685
|
+
this._diffCell(root, newCell?.text, kindNew, false, "new");
|
|
3686
|
+
}
|
|
3687
|
+
else {
|
|
3688
|
+
// Unified: context = one visual row; change = a remove row then an add row.
|
|
3689
|
+
if (kindOld === "context") {
|
|
3690
|
+
this._diffCell(root, oldCell?.lineNumber, "context", true, "old");
|
|
3691
|
+
this._diffCell(root, newCell?.lineNumber, "context", true, "new");
|
|
3692
|
+
this._diffCell(root, oldCell?.text, "context", false, "old");
|
|
3693
|
+
}
|
|
3694
|
+
else {
|
|
3695
|
+
if (oldCell !== null) {
|
|
3696
|
+
this._diffCell(root, oldCell?.lineNumber, "remove", true, "old");
|
|
3697
|
+
this._diffCell(root, "", "remove", true, "new");
|
|
3698
|
+
this._diffCell(root, oldCell?.text, "remove", false, "old");
|
|
3699
|
+
}
|
|
3700
|
+
if (newCell !== null) {
|
|
3701
|
+
this._diffCell(root, "", "add", true, "old");
|
|
3702
|
+
this._diffCell(root, newCell?.lineNumber, "add", true, "new");
|
|
3703
|
+
this._diffCell(root, newCell?.text, "add", false, "new");
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
parent.appendChild(root);
|
|
3709
|
+
}
|
|
3710
|
+
_diffCell(parent, content, kind, isLinenum, side) {
|
|
3711
|
+
const el = document.createElement("div");
|
|
3712
|
+
const parts = ["vms-diff__cell", `vms-diff__cell--${kind}`];
|
|
3713
|
+
if (isLinenum) {
|
|
3714
|
+
parts.push("vms-diff__cell--linenum");
|
|
3715
|
+
parts.push(side === "old" ? "vms-diff__cell--old-linenum" : "vms-diff__cell--new-linenum");
|
|
3716
|
+
// Line numbers are visual gutter — hide from SR (the actual content
|
|
3717
|
+
// announces on the content cell). Also mark unselectable via CSS.
|
|
3718
|
+
el.setAttribute("aria-hidden", "true");
|
|
3719
|
+
}
|
|
3720
|
+
el.className = parts.join(" ");
|
|
3721
|
+
el.textContent = content == null ? "" : String(content);
|
|
3722
|
+
parent.appendChild(el);
|
|
3723
|
+
}
|
|
3611
3724
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface Adapter {
|
|
|
103
103
|
* concept). */
|
|
104
104
|
reload?(): void;
|
|
105
105
|
}
|
|
106
|
-
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode | DividerNode | FitsNode | EmptyStateNode | BadgeNode | ChartNode | BreadcrumbNode | StepsNode | TrackerNode;
|
|
106
|
+
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode | DividerNode | FitsNode | EmptyStateNode | BadgeNode | ChartNode | BreadcrumbNode | StepsNode | TrackerNode | DiffNode;
|
|
107
107
|
export interface PageNode {
|
|
108
108
|
type: "page";
|
|
109
109
|
title?: string;
|
|
@@ -1155,6 +1155,76 @@ export interface TrackerNode {
|
|
|
1155
1155
|
/** Ordered buckets, oldest → newest (rendered left → right). */
|
|
1156
1156
|
cells: TrackerCell[];
|
|
1157
1157
|
}
|
|
1158
|
+
/** One cell of a diff row (either the "old" side or the "new" side). Carries the
|
|
1159
|
+
* cell's text plus an optional line number for the gutter. `text` may be an
|
|
1160
|
+
* empty string. The wire distinction "cell present but empty" vs "cell absent"
|
|
1161
|
+
* (the `null` case on the parent DiffRow) carries meaning — see DiffRow. */
|
|
1162
|
+
export interface DiffCell {
|
|
1163
|
+
text: string;
|
|
1164
|
+
lineNumber?: number;
|
|
1165
|
+
}
|
|
1166
|
+
/** One row of a DiffNode. Row-KIND (add / remove / context) is derived
|
|
1167
|
+
* client-side from the shape of the row itself — no separate `kind` wire field:
|
|
1168
|
+
*
|
|
1169
|
+
* old != null && new == null → REMOVE (left-side content, right side empty)
|
|
1170
|
+
* old == null && new != null → ADD (right-side content, left side empty)
|
|
1171
|
+
* old.text === new.text → CONTEXT (unchanged; both sides identical)
|
|
1172
|
+
* old.text !== new.text → a modified pair — both cells shown side-by-side
|
|
1173
|
+
* and tinted in side-by-side mode; splits into
|
|
1174
|
+
* a REMOVE then ADD row-pair in unified mode
|
|
1175
|
+
*
|
|
1176
|
+
* This "shape carries the meaning" pattern makes it impossible for a `kind`
|
|
1177
|
+
* label to disagree with the content, and keeps the wire compact. Line numbers
|
|
1178
|
+
* are optional so diff sources that don't track them (e.g. two prose versions)
|
|
1179
|
+
* degrade to a content-only column. */
|
|
1180
|
+
export interface DiffRow {
|
|
1181
|
+
/** Old (pre-change) cell. OMIT to signal a pure addition (no left-side content).
|
|
1182
|
+
* Wire contract (gotcha #8): omit the key rather than emit `"old": null` — an
|
|
1183
|
+
* unset optional is ABSENT, never `"field": null`. The renderer treats a
|
|
1184
|
+
* missing key as "no old side" (pure add). */
|
|
1185
|
+
old?: DiffCell;
|
|
1186
|
+
/** New (post-change) cell. OMIT to signal a pure removal (no right-side content).
|
|
1187
|
+
* Same wire contract as `old` — omit the key rather than emit `null`. */
|
|
1188
|
+
new?: DiffCell;
|
|
1189
|
+
}
|
|
1190
|
+
/** DiffNode — aligned before/after primitive for review, audit, and
|
|
1191
|
+
* change-comparison apps. Consumers compute the diff server-side (LibGit2Sharp
|
|
1192
|
+
* on .NET, `diff` lib on TS, `git diff --json`, whatever they have) and hand
|
|
1193
|
+
* VMS the structured rows — same server-computes / framework-renders doctrine
|
|
1194
|
+
* as the markdown → tree pattern. The framework owns ALL appearance and a11y:
|
|
1195
|
+
* the CSS-Grid alignment (4 tracks in side-by-side, 3 in unified), the tint +
|
|
1196
|
+
* left-stripe row coloring, the long-line horizontal-scroll-per-cell that
|
|
1197
|
+
* preserves row alignment, the empty-cell styling, the tinted line numbers,
|
|
1198
|
+
* and the unified-mode collapse of the two linenum columns into a single left
|
|
1199
|
+
* margin. This is the CHARTS/TRACKER precedent applied to before/after
|
|
1200
|
+
* content — aligned side-by-side is a genuinely uncomposable capability that
|
|
1201
|
+
* no combination of existing nodes produces (two `pre` blocks in `layout:"split"`
|
|
1202
|
+
* give you two independent walls of text with no row alignment). Explicitly
|
|
1203
|
+
* out of scope for v1: syntax highlighting on cells (a separate CodeBlockNode
|
|
1204
|
+
* question), word-level intra-line diff highlighting (would need inline rich
|
|
1205
|
+
* text, an open architectural question), in-line review comments, and
|
|
1206
|
+
* collapse/expand of hunks (consumers who want that compute a smaller `rows`
|
|
1207
|
+
* array server-side). Design of record: [design/diff-node.md](../../.planning/design/diff-node.md). */
|
|
1208
|
+
export interface DiffNode {
|
|
1209
|
+
type: "diff";
|
|
1210
|
+
id?: string;
|
|
1211
|
+
/** Ordered rows, top → bottom (rendered in order). */
|
|
1212
|
+
rows: DiffRow[];
|
|
1213
|
+
/** Layout mode. Omitted = "side-by-side" (the default — the whole reason this
|
|
1214
|
+
* primitive exists over composition of two `pre` blocks). "unified" = a
|
|
1215
|
+
* single-column form with dual line-number columns, useful when horizontal
|
|
1216
|
+
* space is constrained or the app prefers the git-log-style presentation.
|
|
1217
|
+
* Closed union (DIFF-01). */
|
|
1218
|
+
mode?: "unified" | "side-by-side";
|
|
1219
|
+
/** Optional header row showing file paths (or any labels) for old/new. In
|
|
1220
|
+
* side-by-side mode: spans the two column-groups (2 tracks each). In unified
|
|
1221
|
+
* mode: spans all 3 tracks and joins the two labels with " → ". Omitted =
|
|
1222
|
+
* no header. */
|
|
1223
|
+
header?: {
|
|
1224
|
+
old: string;
|
|
1225
|
+
new: string;
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1158
1228
|
export interface ShellOptions {
|
|
1159
1229
|
endpoint: string;
|
|
1160
1230
|
actionEndpoint: string;
|
package/dist/server.js
CHANGED
|
@@ -239,11 +239,11 @@ function collectActions(node, enclosingForm, out) {
|
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
241
|
// Nodes with no dispatch-bearing actions of their own:
|
|
242
|
-
// text, link, image, stat-bar, progress, copy-button, badge, chart, steps
|
|
242
|
+
// text, link, image, stat-bar, progress, copy-button, badge, chart, steps, diff
|
|
243
243
|
// (breadcrumb crumb actions ARE recorded above via the "breadcrumb" arm)
|
|
244
|
-
// ChartNode (CHART-05)
|
|
245
|
-
// childless/action-free leaves — they carry only data, so they
|
|
246
|
-
// here with no recursion (no fits-style blind spot).
|
|
244
|
+
// ChartNode (CHART-05), StepsNode (NAV-02), and DiffNode (DIFF-01) are
|
|
245
|
+
// DELIBERATE childless/action-free leaves — they carry only data, so they
|
|
246
|
+
// fall through here with no recursion (no fits-style blind spot).
|
|
247
247
|
default:
|
|
248
248
|
return;
|
|
249
249
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "6.
|
|
4
|
-
"description": "A server-driven UI framework where the wire format is structured enough that agents can build full-stack apps without ever opening a browser and all UI tests are pure unit tests with no browser runtime. Server returns a JSON tree of typed nodes; a thin TypeScript adapter renders it to DOM. Backend-agnostic
|
|
3
|
+
"version": "6.8.0",
|
|
4
|
+
"description": "A server-driven UI framework where the wire format is structured enough that agents can build full-stack apps without ever opening a browser and all UI tests are pure unit tests with no browser runtime. Server returns a JSON tree of typed nodes; a thin TypeScript adapter renders it to DOM. Backend-agnostic \u2014 a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "ashley-shrok",
|
package/styles/default.css
CHANGED
|
@@ -1783,3 +1783,109 @@ button.vms-breadcrumb__link {
|
|
|
1783
1783
|
position: relative; /* lift the ring above neighbors */
|
|
1784
1784
|
z-index: 1;
|
|
1785
1785
|
}
|
|
1786
|
+
|
|
1787
|
+
/* ── DiffNode ────────────────────────────────────────────────────────────────
|
|
1788
|
+
Aligned before/after primitive. CSS Grid handles vertical alignment across
|
|
1789
|
+
two columns naturally (each row's cells share a grid row). Long lines
|
|
1790
|
+
overflow HORIZONTALLY within their cell (per-cell overflow-x: auto) — never
|
|
1791
|
+
wrap, because wrapping would break the row-alignment guarantee. Row-kind
|
|
1792
|
+
coloring is DUAL SIGNAL: background tint (22% alpha) + a 3px inset left
|
|
1793
|
+
stripe on the leftmost cell of each colored row-side. Spike-validated
|
|
1794
|
+
2026-07-19; design of record: .planning/design/diff-node.md. */
|
|
1795
|
+
.vms-diff {
|
|
1796
|
+
display: grid;
|
|
1797
|
+
font-family: var(--vms-font-mono, ui-monospace, SFMono-Regular, monospace);
|
|
1798
|
+
font-size: var(--vms-text-sm);
|
|
1799
|
+
line-height: 1.5;
|
|
1800
|
+
border: 1px solid var(--vms-border);
|
|
1801
|
+
border-radius: var(--vms-radius-md, 6px);
|
|
1802
|
+
overflow: hidden;
|
|
1803
|
+
background: var(--vms-surface);
|
|
1804
|
+
color: var(--vms-text);
|
|
1805
|
+
}
|
|
1806
|
+
/* Side-by-side: 4 tracks (old-line#, old-content, new-line#, new-content). */
|
|
1807
|
+
.vms-diff--side-by-side {
|
|
1808
|
+
grid-template-columns:
|
|
1809
|
+
[old-line] max-content
|
|
1810
|
+
[old-content] minmax(0, 1fr)
|
|
1811
|
+
[new-line] max-content
|
|
1812
|
+
[new-content] minmax(0, 1fr);
|
|
1813
|
+
}
|
|
1814
|
+
/* Unified: 3 tracks (dual line#s + one content column). */
|
|
1815
|
+
.vms-diff--unified {
|
|
1816
|
+
grid-template-columns:
|
|
1817
|
+
[line-old] max-content
|
|
1818
|
+
[line-new] max-content
|
|
1819
|
+
[content] minmax(0, 1fr);
|
|
1820
|
+
}
|
|
1821
|
+
.vms-diff__cell {
|
|
1822
|
+
padding: 0.15em 0.6em;
|
|
1823
|
+
white-space: pre;
|
|
1824
|
+
overflow-x: auto;
|
|
1825
|
+
min-width: 0; /* required inside minmax(0,1fr) so overflow doesn't blow the track */
|
|
1826
|
+
}
|
|
1827
|
+
/* Line-number cells (base — neutral tint; add/remove/empty override below). */
|
|
1828
|
+
.vms-diff__cell--linenum {
|
|
1829
|
+
color: var(--vms-text-muted);
|
|
1830
|
+
text-align: right;
|
|
1831
|
+
user-select: none;
|
|
1832
|
+
padding: 0.15em 0.5em;
|
|
1833
|
+
border-right: 1px solid var(--vms-border);
|
|
1834
|
+
background: color-mix(in srgb, var(--vms-text-muted) 4%, transparent);
|
|
1835
|
+
}
|
|
1836
|
+
/* Row-kind coloring — applied to BOTH linenum and content cells so each row-side
|
|
1837
|
+
reads as one connected colored band. Stripe on the leftmost cell only. */
|
|
1838
|
+
.vms-diff__cell--add {
|
|
1839
|
+
background: color-mix(in srgb, var(--vms-success) 22%, transparent);
|
|
1840
|
+
}
|
|
1841
|
+
.vms-diff__cell--remove {
|
|
1842
|
+
background: color-mix(in srgb, var(--vms-error) 22%, transparent);
|
|
1843
|
+
}
|
|
1844
|
+
.vms-diff__cell--linenum.vms-diff__cell--add {
|
|
1845
|
+
box-shadow: inset 3px 0 0 0 var(--vms-success);
|
|
1846
|
+
color: var(--vms-success);
|
|
1847
|
+
}
|
|
1848
|
+
.vms-diff__cell--linenum.vms-diff__cell--remove {
|
|
1849
|
+
box-shadow: inset 3px 0 0 0 var(--vms-error);
|
|
1850
|
+
color: var(--vms-error);
|
|
1851
|
+
}
|
|
1852
|
+
.vms-diff__cell--context {
|
|
1853
|
+
/* neutral — no fill */
|
|
1854
|
+
}
|
|
1855
|
+
.vms-diff__cell--empty {
|
|
1856
|
+
background: color-mix(in srgb, var(--vms-text-muted) 6%, transparent);
|
|
1857
|
+
}
|
|
1858
|
+
/* Unified mode — collapse the two linenum columns visually into one left margin.
|
|
1859
|
+
Context rows: only leftmost linenum keeps muted base + border-right (one clean
|
|
1860
|
+
line-# margin). Add/remove rows: the second linenum keeps its row-tint so the
|
|
1861
|
+
color band runs continuously across [line#][line#][content], but drops the
|
|
1862
|
+
stripe (which belongs only on the leftmost cell of the row). */
|
|
1863
|
+
.vms-diff--unified .vms-diff__cell--new-linenum {
|
|
1864
|
+
background: transparent;
|
|
1865
|
+
border-right: none;
|
|
1866
|
+
}
|
|
1867
|
+
.vms-diff--unified .vms-diff__cell--new-linenum.vms-diff__cell--add {
|
|
1868
|
+
background: color-mix(in srgb, var(--vms-success) 22%, transparent);
|
|
1869
|
+
box-shadow: none;
|
|
1870
|
+
}
|
|
1871
|
+
.vms-diff--unified .vms-diff__cell--new-linenum.vms-diff__cell--remove {
|
|
1872
|
+
background: color-mix(in srgb, var(--vms-error) 22%, transparent);
|
|
1873
|
+
box-shadow: none;
|
|
1874
|
+
}
|
|
1875
|
+
/* Header (optional file-path row). */
|
|
1876
|
+
.vms-diff__header {
|
|
1877
|
+
padding: 0.4em 0.8em;
|
|
1878
|
+
font-weight: 600;
|
|
1879
|
+
background: color-mix(in srgb, var(--vms-text-muted) 8%, transparent);
|
|
1880
|
+
border-bottom: 1px solid var(--vms-border);
|
|
1881
|
+
}
|
|
1882
|
+
.vms-diff--side-by-side .vms-diff__header--old { grid-column: old-line / span 2; }
|
|
1883
|
+
.vms-diff--side-by-side .vms-diff__header--new {
|
|
1884
|
+
grid-column: new-line / span 2;
|
|
1885
|
+
border-left: 1px solid var(--vms-border);
|
|
1886
|
+
}
|
|
1887
|
+
.vms-diff--unified .vms-diff__header { grid-column: 1 / -1; }
|
|
1888
|
+
/* Vertical divider between old and new in side-by-side mode. */
|
|
1889
|
+
.vms-diff--side-by-side .vms-diff__cell--new-linenum {
|
|
1890
|
+
border-left: 1px solid var(--vms-border);
|
|
1891
|
+
}
|