@ashley-shrok/viewmodel-shell 6.3.0 → 6.4.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.js +67 -0
- package/dist/index.d.ts +25 -0
- package/package.json +1 -1
- package/styles/default.css +8 -0
package/dist/browser.js
CHANGED
|
@@ -3025,6 +3025,45 @@ export class BrowserAdapter {
|
|
|
3025
3025
|
if (tableHasCheckboxes) {
|
|
3026
3026
|
const selTh = document.createElement("th");
|
|
3027
3027
|
selTh.className = "vms-table__th vms-table__th--select";
|
|
3028
|
+
// Header "select all rendered rows" affordance. A pure client-side toggle
|
|
3029
|
+
// over the leading-column checkboxes: it writes the SAME per-row binds the
|
|
3030
|
+
// row checkboxes use, so the server learns the selection through the exact
|
|
3031
|
+
// path it already knows — no new wire field, and agents (which set binds
|
|
3032
|
+
// directly) never need it. Scope is deliberately "all RENDERED rows": under
|
|
3033
|
+
// filter-narrow that equals all matches; under pagination it is the current
|
|
3034
|
+
// page (selection accumulates via the round-tripped binds); over-cap /
|
|
3035
|
+
// 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. This re-adds ONLY
|
|
3037
|
+
// the DOM toggle, never the per-toggle dispatch that got the old
|
|
3038
|
+
// TableNode.selection seam removed in 0.15.0.
|
|
3039
|
+
const rowCheckboxes = n.rows.flatMap(r => (r.actions ?? []).filter((e) => e.type === "checkbox"));
|
|
3040
|
+
if (rowCheckboxes.length > 0) {
|
|
3041
|
+
const lbl = document.createElement("label");
|
|
3042
|
+
lbl.className = "vms-checkbox vms-table__select-all";
|
|
3043
|
+
const inp = document.createElement("input");
|
|
3044
|
+
inp.type = "checkbox";
|
|
3045
|
+
inp.className = "vms-checkbox__input";
|
|
3046
|
+
inp.setAttribute("aria-label", "Select all rows");
|
|
3047
|
+
const states = rowCheckboxes.map(cb => Boolean(this.sa.read(cb.bind)));
|
|
3048
|
+
const allChecked = states.every(Boolean);
|
|
3049
|
+
inp.checked = allChecked;
|
|
3050
|
+
inp.indeterminate = !allChecked && states.some(Boolean);
|
|
3051
|
+
const mark = document.createElement("span");
|
|
3052
|
+
mark.className = "vms-checkbox__mark";
|
|
3053
|
+
inp.addEventListener("change", () => {
|
|
3054
|
+
const target = inp.checked;
|
|
3055
|
+
for (const cb of rowCheckboxes) {
|
|
3056
|
+
this.sa.write(cb.bind, target);
|
|
3057
|
+
const rowInp = document.getElementById(`vms-checkbox-${cb.name}`);
|
|
3058
|
+
if (rowInp)
|
|
3059
|
+
rowInp.checked = target;
|
|
3060
|
+
}
|
|
3061
|
+
inp.indeterminate = false;
|
|
3062
|
+
});
|
|
3063
|
+
lbl.appendChild(inp);
|
|
3064
|
+
lbl.appendChild(mark);
|
|
3065
|
+
selTh.appendChild(lbl);
|
|
3066
|
+
}
|
|
3028
3067
|
headerRow.appendChild(selTh);
|
|
3029
3068
|
}
|
|
3030
3069
|
const sortIntent = (n.sortBind != null ? this.sa.read(n.sortBind) : null);
|
|
@@ -3198,6 +3237,34 @@ export class BrowserAdapter {
|
|
|
3198
3237
|
});
|
|
3199
3238
|
table.appendChild(tbody);
|
|
3200
3239
|
wrapper.appendChild(table);
|
|
3240
|
+
// Visible-scoped bulk-action toolbar (n.selection). Rendered ABOVE the table.
|
|
3241
|
+
// Each button, on click, harvests the currently-CHECKED, currently-RENDERED
|
|
3242
|
+
// rows (by walking tbody for a checked leading-column checkbox and reading the
|
|
3243
|
+
// row's data-id) and writes that id array to n.selection.harvestBind —
|
|
3244
|
+
// OVERWRITING — before dispatching. So the server only ever sees rows the user
|
|
3245
|
+
// can currently see; a row selected then filtered/paginated out of view is not
|
|
3246
|
+
// harvested. This revives the old selection.buttons[] harvest (removed with the
|
|
3247
|
+
// `context` wire in Phase 6), adapted to write a BIND instead of context. It
|
|
3248
|
+
// adds NONE of the per-toggle dispatch that got the 0.15.0 selection.action
|
|
3249
|
+
// seam removed — selection stays a pure client concern until a bulk click.
|
|
3250
|
+
if (n.selection && n.selection.buttons.length > 0) {
|
|
3251
|
+
const sel = n.selection;
|
|
3252
|
+
const toolbar = document.createElement("div");
|
|
3253
|
+
toolbar.className = "vms-table__bulk-actions";
|
|
3254
|
+
const harvest = (action) => {
|
|
3255
|
+
const ids = [];
|
|
3256
|
+
tbody.querySelectorAll("tr").forEach(tr => {
|
|
3257
|
+
const box = tr.querySelector(".vms-table__td--select input.vms-checkbox__input");
|
|
3258
|
+
if (box?.checked && tr.dataset.id)
|
|
3259
|
+
ids.push(tr.dataset.id);
|
|
3260
|
+
});
|
|
3261
|
+
this.sa.write(sel.harvestBind, ids);
|
|
3262
|
+
on({ name: action.name });
|
|
3263
|
+
};
|
|
3264
|
+
for (const btn of sel.buttons)
|
|
3265
|
+
this.button(btn, toolbar, harvest);
|
|
3266
|
+
wrapper.insertBefore(toolbar, table);
|
|
3267
|
+
}
|
|
3201
3268
|
if (n.pagination) {
|
|
3202
3269
|
const pg = n.pagination;
|
|
3203
3270
|
const footer = document.createElement("div");
|
package/dist/index.d.ts
CHANGED
|
@@ -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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
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 — a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
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
|