@ashley-shrok/viewmodel-shell 6.3.0 → 6.5.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 +160 -0
- package/dist/index.d.ts +96 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/styles/default.css +114 -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
|
|
@@ -3025,6 +3026,45 @@ export class BrowserAdapter {
|
|
|
3025
3026
|
if (tableHasCheckboxes) {
|
|
3026
3027
|
const selTh = document.createElement("th");
|
|
3027
3028
|
selTh.className = "vms-table__th vms-table__th--select";
|
|
3029
|
+
// Header "select all rendered rows" affordance. A pure client-side toggle
|
|
3030
|
+
// over the leading-column checkboxes: it writes the SAME per-row binds the
|
|
3031
|
+
// row checkboxes use, so the server learns the selection through the exact
|
|
3032
|
+
// path it already knows — no new wire field, and agents (which set binds
|
|
3033
|
+
// directly) never need it. Scope is deliberately "all RENDERED rows": under
|
|
3034
|
+
// filter-narrow that equals all matches; under pagination it is the current
|
|
3035
|
+
// page (selection accumulates via the round-tripped binds); over-cap /
|
|
3036
|
+
// zero-match render no rows, so the control below simply is not drawn and
|
|
3037
|
+
// can never claim to select rows that are not on screen. This re-adds ONLY
|
|
3038
|
+
// the DOM toggle, never the per-toggle dispatch that got the old
|
|
3039
|
+
// TableNode.selection seam removed in 0.15.0.
|
|
3040
|
+
const rowCheckboxes = n.rows.flatMap(r => (r.actions ?? []).filter((e) => e.type === "checkbox"));
|
|
3041
|
+
if (rowCheckboxes.length > 0) {
|
|
3042
|
+
const lbl = document.createElement("label");
|
|
3043
|
+
lbl.className = "vms-checkbox vms-table__select-all";
|
|
3044
|
+
const inp = document.createElement("input");
|
|
3045
|
+
inp.type = "checkbox";
|
|
3046
|
+
inp.className = "vms-checkbox__input";
|
|
3047
|
+
inp.setAttribute("aria-label", "Select all rows");
|
|
3048
|
+
const states = rowCheckboxes.map(cb => Boolean(this.sa.read(cb.bind)));
|
|
3049
|
+
const allChecked = states.every(Boolean);
|
|
3050
|
+
inp.checked = allChecked;
|
|
3051
|
+
inp.indeterminate = !allChecked && states.some(Boolean);
|
|
3052
|
+
const mark = document.createElement("span");
|
|
3053
|
+
mark.className = "vms-checkbox__mark";
|
|
3054
|
+
inp.addEventListener("change", () => {
|
|
3055
|
+
const target = inp.checked;
|
|
3056
|
+
for (const cb of rowCheckboxes) {
|
|
3057
|
+
this.sa.write(cb.bind, target);
|
|
3058
|
+
const rowInp = document.getElementById(`vms-checkbox-${cb.name}`);
|
|
3059
|
+
if (rowInp)
|
|
3060
|
+
rowInp.checked = target;
|
|
3061
|
+
}
|
|
3062
|
+
inp.indeterminate = false;
|
|
3063
|
+
});
|
|
3064
|
+
lbl.appendChild(inp);
|
|
3065
|
+
lbl.appendChild(mark);
|
|
3066
|
+
selTh.appendChild(lbl);
|
|
3067
|
+
}
|
|
3028
3068
|
headerRow.appendChild(selTh);
|
|
3029
3069
|
}
|
|
3030
3070
|
const sortIntent = (n.sortBind != null ? this.sa.read(n.sortBind) : null);
|
|
@@ -3198,6 +3238,34 @@ export class BrowserAdapter {
|
|
|
3198
3238
|
});
|
|
3199
3239
|
table.appendChild(tbody);
|
|
3200
3240
|
wrapper.appendChild(table);
|
|
3241
|
+
// Visible-scoped bulk-action toolbar (n.selection). Rendered ABOVE the table.
|
|
3242
|
+
// Each button, on click, harvests the currently-CHECKED, currently-RENDERED
|
|
3243
|
+
// rows (by walking tbody for a checked leading-column checkbox and reading the
|
|
3244
|
+
// row's data-id) and writes that id array to n.selection.harvestBind —
|
|
3245
|
+
// OVERWRITING — before dispatching. So the server only ever sees rows the user
|
|
3246
|
+
// can currently see; a row selected then filtered/paginated out of view is not
|
|
3247
|
+
// harvested. This revives the old selection.buttons[] harvest (removed with the
|
|
3248
|
+
// `context` wire in Phase 6), adapted to write a BIND instead of context. It
|
|
3249
|
+
// adds NONE of the per-toggle dispatch that got the 0.15.0 selection.action
|
|
3250
|
+
// seam removed — selection stays a pure client concern until a bulk click.
|
|
3251
|
+
if (n.selection && n.selection.buttons.length > 0) {
|
|
3252
|
+
const sel = n.selection;
|
|
3253
|
+
const toolbar = document.createElement("div");
|
|
3254
|
+
toolbar.className = "vms-table__bulk-actions";
|
|
3255
|
+
const harvest = (action) => {
|
|
3256
|
+
const ids = [];
|
|
3257
|
+
tbody.querySelectorAll("tr").forEach(tr => {
|
|
3258
|
+
const box = tr.querySelector(".vms-table__td--select input.vms-checkbox__input");
|
|
3259
|
+
if (box?.checked && tr.dataset.id)
|
|
3260
|
+
ids.push(tr.dataset.id);
|
|
3261
|
+
});
|
|
3262
|
+
this.sa.write(sel.harvestBind, ids);
|
|
3263
|
+
on({ name: action.name });
|
|
3264
|
+
};
|
|
3265
|
+
for (const btn of sel.buttons)
|
|
3266
|
+
this.button(btn, toolbar, harvest);
|
|
3267
|
+
wrapper.insertBefore(toolbar, table);
|
|
3268
|
+
}
|
|
3201
3269
|
if (n.pagination) {
|
|
3202
3270
|
const pg = n.pagination;
|
|
3203
3271
|
const footer = document.createElement("div");
|
|
@@ -3541,4 +3609,96 @@ export class BrowserAdapter {
|
|
|
3541
3609
|
}
|
|
3542
3610
|
parent.appendChild(strip);
|
|
3543
3611
|
}
|
|
3612
|
+
/** DiffNode — aligned before/after primitive. Row-kind (add/remove/context)
|
|
3613
|
+
* is derived client-side from the shape of DiffRow: old-only = remove,
|
|
3614
|
+
* new-only = add, matching text = context, differing text = modified pair
|
|
3615
|
+
* (side-by-side shows both cells; unified splits into remove-then-add).
|
|
3616
|
+
* Line-number cells carry the same kind class as their content cell so the
|
|
3617
|
+
* whole row-side reads as one connected colored band. Left-stripe lives only
|
|
3618
|
+
* on the leftmost cell of each colored row-side (linenum cell). In unified
|
|
3619
|
+
* mode the two linenum columns visually collapse into a single left margin
|
|
3620
|
+
* for context rows; for add/remove rows the second linenum keeps its tint so
|
|
3621
|
+
* the color band runs continuously, minus the stripe. Design of record:
|
|
3622
|
+
* `.planning/design/diff-node.md`. Spike-validated 2026-07-19. */
|
|
3623
|
+
diff(n, parent) {
|
|
3624
|
+
const mode = n.mode ?? "side-by-side";
|
|
3625
|
+
const root = document.createElement("div");
|
|
3626
|
+
root.className = `vms-diff vms-diff--${mode}`;
|
|
3627
|
+
if (n.id != null)
|
|
3628
|
+
root.id = n.id;
|
|
3629
|
+
root.setAttribute("role", "group");
|
|
3630
|
+
root.setAttribute("aria-label", "Diff");
|
|
3631
|
+
// Optional header row(s) — file paths for old / new.
|
|
3632
|
+
if (n.header) {
|
|
3633
|
+
if (mode === "side-by-side") {
|
|
3634
|
+
const hOld = document.createElement("div");
|
|
3635
|
+
hOld.className = "vms-diff__header vms-diff__header--old";
|
|
3636
|
+
hOld.textContent = n.header.old;
|
|
3637
|
+
root.appendChild(hOld);
|
|
3638
|
+
const hNew = document.createElement("div");
|
|
3639
|
+
hNew.className = "vms-diff__header vms-diff__header--new";
|
|
3640
|
+
hNew.textContent = n.header.new;
|
|
3641
|
+
root.appendChild(hNew);
|
|
3642
|
+
}
|
|
3643
|
+
else {
|
|
3644
|
+
const h = document.createElement("div");
|
|
3645
|
+
h.className = "vms-diff__header";
|
|
3646
|
+
h.textContent = `${n.header.old} → ${n.header.new}`;
|
|
3647
|
+
root.appendChild(h);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
for (const row of n.rows) {
|
|
3651
|
+
const oldCell = row.old ?? null;
|
|
3652
|
+
const newCell = row.new ?? null;
|
|
3653
|
+
const kindOld = oldCell == null ? "empty"
|
|
3654
|
+
: newCell == null ? "remove"
|
|
3655
|
+
: oldCell.text === newCell.text ? "context"
|
|
3656
|
+
: "remove";
|
|
3657
|
+
const kindNew = newCell == null ? "empty"
|
|
3658
|
+
: oldCell == null ? "add"
|
|
3659
|
+
: newCell.text === oldCell.text ? "context"
|
|
3660
|
+
: "add";
|
|
3661
|
+
if (mode === "side-by-side") {
|
|
3662
|
+
this._diffCell(root, oldCell?.lineNumber, kindOld, true, "old");
|
|
3663
|
+
this._diffCell(root, oldCell?.text, kindOld, false, "old");
|
|
3664
|
+
this._diffCell(root, newCell?.lineNumber, kindNew, true, "new");
|
|
3665
|
+
this._diffCell(root, newCell?.text, kindNew, false, "new");
|
|
3666
|
+
}
|
|
3667
|
+
else {
|
|
3668
|
+
// Unified: context = one visual row; change = a remove row then an add row.
|
|
3669
|
+
if (kindOld === "context") {
|
|
3670
|
+
this._diffCell(root, oldCell?.lineNumber, "context", true, "old");
|
|
3671
|
+
this._diffCell(root, newCell?.lineNumber, "context", true, "new");
|
|
3672
|
+
this._diffCell(root, oldCell?.text, "context", false, "old");
|
|
3673
|
+
}
|
|
3674
|
+
else {
|
|
3675
|
+
if (oldCell !== null) {
|
|
3676
|
+
this._diffCell(root, oldCell?.lineNumber, "remove", true, "old");
|
|
3677
|
+
this._diffCell(root, "", "remove", true, "new");
|
|
3678
|
+
this._diffCell(root, oldCell?.text, "remove", false, "old");
|
|
3679
|
+
}
|
|
3680
|
+
if (newCell !== null) {
|
|
3681
|
+
this._diffCell(root, "", "add", true, "old");
|
|
3682
|
+
this._diffCell(root, newCell?.lineNumber, "add", true, "new");
|
|
3683
|
+
this._diffCell(root, newCell?.text, "add", false, "new");
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
parent.appendChild(root);
|
|
3689
|
+
}
|
|
3690
|
+
_diffCell(parent, content, kind, isLinenum, side) {
|
|
3691
|
+
const el = document.createElement("div");
|
|
3692
|
+
const parts = ["vms-diff__cell", `vms-diff__cell--${kind}`];
|
|
3693
|
+
if (isLinenum) {
|
|
3694
|
+
parts.push("vms-diff__cell--linenum");
|
|
3695
|
+
parts.push(side === "old" ? "vms-diff__cell--old-linenum" : "vms-diff__cell--new-linenum");
|
|
3696
|
+
// Line numbers are visual gutter — hide from SR (the actual content
|
|
3697
|
+
// announces on the content cell). Also mark unselectable via CSS.
|
|
3698
|
+
el.setAttribute("aria-hidden", "true");
|
|
3699
|
+
}
|
|
3700
|
+
el.className = parts.join(" ");
|
|
3701
|
+
el.textContent = content == null ? "" : String(content);
|
|
3702
|
+
parent.appendChild(el);
|
|
3703
|
+
}
|
|
3544
3704
|
}
|
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;
|
|
@@ -868,6 +868,31 @@ export interface TableNode {
|
|
|
868
868
|
* filter dispatches reset `page` to 1 on the server side, since
|
|
869
869
|
* the row window changes underneath them. */
|
|
870
870
|
pagination?: TablePagination;
|
|
871
|
+
/** Visible-scoped bulk-action toolbar (opt-in). When set, the adapter renders
|
|
872
|
+
* `selection.buttons[]` ABOVE the table; each button, on click, harvests the
|
|
873
|
+
* currently-CHECKED, currently-RENDERED row ids (from the leading-column
|
|
874
|
+
* per-row checkboxes) and writes them — a `string[]` of `TableRow.id` — to
|
|
875
|
+
* `selection.harvestBind`, OVERWRITING, before dispatching its own action.
|
|
876
|
+
* The server reads that path to act, so a bulk action can only ever affect
|
|
877
|
+
* rows the user can currently see: a row selected under one filter and then
|
|
878
|
+
* filtered/paginated out of view is NOT harvested. This is the safe default
|
|
879
|
+
* for the common case; an app that genuinely wants cross-page/persistent
|
|
880
|
+
* selection simply ignores this block and reads its own bound `selectedIds`
|
|
881
|
+
* map instead (the framework never introspects app state, so that escape
|
|
882
|
+
* hatch is always open). Selectable rows must carry `TableRow.id`. */
|
|
883
|
+
selection?: TableSelection;
|
|
884
|
+
}
|
|
885
|
+
/** Visible-scoped bulk-action toolbar for a `TableNode` — see `TableNode.selection`. */
|
|
886
|
+
export interface TableSelection {
|
|
887
|
+
/** Bulk-action buttons rendered above the table. On click, each harvests the
|
|
888
|
+
* visible-checked row ids into `harvestBind` (overwriting) and dispatches its
|
|
889
|
+
* own `action` — name-only, per the Phase-6 wire. Full ButtonNodes, so they
|
|
890
|
+
* carry their own emphasis/tone/confirm/disabled. */
|
|
891
|
+
buttons: ButtonNode[];
|
|
892
|
+
/** State path the harvest writes the visible-checked row-id array (`string[]`)
|
|
893
|
+
* to right before dispatch. The server reads THIS to act — not a per-row
|
|
894
|
+
* `selectedIds` map — which is what makes the action visible-scoped. */
|
|
895
|
+
harvestBind: string;
|
|
871
896
|
}
|
|
872
897
|
export interface CopyButtonNode {
|
|
873
898
|
type: "copy-button";
|
|
@@ -1130,6 +1155,76 @@ export interface TrackerNode {
|
|
|
1130
1155
|
/** Ordered buckets, oldest → newest (rendered left → right). */
|
|
1131
1156
|
cells: TrackerCell[];
|
|
1132
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
|
+
}
|
|
1133
1228
|
export interface ShellOptions {
|
|
1134
1229
|
endpoint: string;
|
|
1135
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.5.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
|
@@ -884,6 +884,14 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
884
884
|
border-left: none;
|
|
885
885
|
transform: rotate(40deg);
|
|
886
886
|
}
|
|
887
|
+
.vms-checkbox__input:indeterminate + .vms-checkbox__mark { background: var(--vms-accent); border-color: var(--vms-accent); }
|
|
888
|
+
.vms-checkbox__input:indeterminate + .vms-checkbox__mark::after {
|
|
889
|
+
content: '';
|
|
890
|
+
position: absolute;
|
|
891
|
+
left: 3px; top: 7px;
|
|
892
|
+
width: 10px; height: 0;
|
|
893
|
+
border-top: 2px solid #fff;
|
|
894
|
+
}
|
|
887
895
|
.vms-checkbox__label { margin-left: var(--vms-space-xs); font-size: var(--vms-text-base); }
|
|
888
896
|
|
|
889
897
|
/* ── Buttons — three orthogonal axes: emphasis (fill) × tone (color) × size
|
|
@@ -1775,3 +1783,109 @@ button.vms-breadcrumb__link {
|
|
|
1775
1783
|
position: relative; /* lift the ring above neighbors */
|
|
1776
1784
|
z-index: 1;
|
|
1777
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
|
+
}
|