@ashley-shrok/viewmodel-shell 0.14.0 → 0.15.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 CHANGED
@@ -674,24 +674,18 @@ export class BrowserAdapter {
674
674
  box.checked = allOnPage;
675
675
  box.indeterminate = someOnPage && !allOnPage;
676
676
  box.addEventListener("change", () => {
677
- const selAction = sel.action;
678
- if (selAction) {
679
- // Server-truth mode: dispatch; server re-renders with new selectedIds.
680
- on({ name: selAction.name, context: { ...(selAction.context ?? {}), all: true, checked: box.checked } });
681
- }
682
- else {
683
- // Local mode (0.13.0): toggle every row checkbox + class in this table
684
- // to match the header. No dispatch — the dispatch guard can't drop anything.
685
- const want = box.checked;
686
- table.querySelectorAll("tbody input.vms-table__select").forEach(rowBox => {
687
- if (rowBox.disabled)
688
- return;
689
- rowBox.checked = want;
690
- const tr = rowBox.closest(".vms-table__row");
691
- if (tr)
692
- tr.classList.toggle("vms-table__row--selected", want);
693
- });
694
- }
677
+ // Toggle every row checkbox + class in this table to match the header.
678
+ // No dispatch — selection is purely DOM-local; bulk actions harvest the
679
+ // checked rows when a `selection.buttons[]` entry is clicked.
680
+ const want = box.checked;
681
+ table.querySelectorAll("tbody input.vms-table__select").forEach(rowBox => {
682
+ if (rowBox.disabled)
683
+ return;
684
+ rowBox.checked = want;
685
+ const tr = rowBox.closest(".vms-table__row");
686
+ if (tr)
687
+ tr.classList.toggle("vms-table__row--selected", want);
688
+ });
695
689
  });
696
690
  th.appendChild(box);
697
691
  headerRow.appendChild(th);
@@ -783,23 +777,18 @@ export class BrowserAdapter {
783
777
  // data-id is what selection.buttons[] harvest reads on click.
784
778
  box.dataset.id = rowId;
785
779
  box.addEventListener("change", () => {
786
- const selAction = sel.action;
787
- if (selAction) {
788
- on({ name: selAction.name, context: { ...(selAction.context ?? {}), id: rowId, checked: box.checked } });
789
- }
790
- else {
791
- // Local mode (0.13.0): flip the row class to mirror the box, then
792
- // reconcile the header select-all (could now be all / some / none).
793
- tr.classList.toggle("vms-table__row--selected", box.checked);
794
- const headerBox = table.querySelector("thead input.vms-table__select--all");
795
- if (headerBox) {
796
- const all = table.querySelectorAll("tbody input.vms-table__select:not(:disabled)");
797
- let checked = 0;
798
- all.forEach(b => { if (b.checked)
799
- checked++; });
800
- headerBox.checked = all.length > 0 && checked === all.length;
801
- headerBox.indeterminate = checked > 0 && checked < all.length;
802
- }
780
+ // Flip the row class to mirror the box, then reconcile the header
781
+ // select-all (could now be all / some / none). No dispatch — bulk
782
+ // actions read the DOM via the selection.buttons[] harvest.
783
+ tr.classList.toggle("vms-table__row--selected", box.checked);
784
+ const headerBox = table.querySelector("thead input.vms-table__select--all");
785
+ if (headerBox) {
786
+ const all = table.querySelectorAll("tbody input.vms-table__select:not(:disabled)");
787
+ let checked = 0;
788
+ all.forEach(b => { if (b.checked)
789
+ checked++; });
790
+ headerBox.checked = all.length > 0 && checked === all.length;
791
+ headerBox.indeterminate = checked > 0 && checked < all.length;
803
792
  }
804
793
  });
805
794
  }
package/dist/index.d.ts CHANGED
@@ -213,31 +213,19 @@ export interface TableRow {
213
213
  variant?: string;
214
214
  }
215
215
  export interface TableSelection {
216
- /** Row ids that should render PRE-SELECTED on this render. In server-truth
217
- * mode (`action` set) this is the live selection, round-tripped in state and
218
- * authoritative every render. In local mode (`action` omitted) this is the
219
- * server's initial pre-selection only — subsequent toggles are purely
220
- * client-side DOM state and the server doesn't see them until a `buttons[]`
221
- * click harvests them. */
216
+ /** Row ids that should render PRE-SELECTED on initial render the server's
217
+ * initial pre-selection. Subsequent toggles are purely client-side DOM state;
218
+ * the server doesn't see them until a `buttons[]` click harvests them. */
222
219
  selectedIds: string[];
223
- /** OPTIONAL (0.13.0). When present: server-truth mode every checkbox toggle
224
- * dispatches this action with merged `{ id, checked }` per row or
225
- * `{ all: true, checked }` for the header select-all (where "all" = the
226
- * rendered page). Selection survives sort/filter/pagination via the state
227
- * round-trip. When OMITTED: local mode the adapter toggles the DOM
228
- * checkbox + `.vms-table__row--selected` class purely client-side with no
229
- * dispatch. Local mode is the recommended pattern for rapid-selection
230
- * workflows (no dropped clicks under the dispatch guard); see `buttons` for
231
- * how bulk actions read the resulting selection. */
232
- action?: ActionEvent;
233
- /** OPTIONAL (0.13.0). When present, the adapter renders these as a bulk-action
234
- * toolbar ABOVE the table. On click, each button harvests the currently
235
- * checked rows from the DOM and dispatches its `action` with
236
- * `{ selectedIds: [...] }` merged into its `context`. Designed primarily to
237
- * pair with local mode (`action` absent) — it's how the server learns the
238
- * selection without a per-toggle round-trip — but works in server-truth mode
239
- * too (the harvest matches `selectedIds` since the DOM reflects server-truth
240
- * after each render). */
220
+ /** When present, the adapter renders these as a bulk-action toolbar ABOVE
221
+ * the table. On click, each button harvests the currently-checked rows from
222
+ * the DOM and dispatches its `action` with `{ selectedIds: [...] }` merged
223
+ * into its `context`. This is the only way to act on the selection — there
224
+ * is no per-toggle dispatch (0.15.0 removed the `action` mode that did,
225
+ * because rapid clicks were dropped by the dispatch guard and the in-flight
226
+ * response wiped the DOM). If a future release needs cross-page persistence
227
+ * or live "N selected" indicators, the way back is a redesigned wire shape
228
+ * (dispatch queueing + optimistic preservation), not the old `action` mode. */
241
229
  buttons?: ButtonNode[];
242
230
  }
243
231
  export interface TablePagination {
package/dist/tui.js CHANGED
@@ -914,30 +914,15 @@ function TableView({ node, ctx }) {
914
914
  context: { ...(node.sortAction.context ?? {}), column: columnKey, direction },
915
915
  });
916
916
  };
917
- // Selection — leading [x]/[ ] column. Dispatch payloads match BrowserAdapter:
918
- // { id, checked } per row, { all: true, checked } for select-all (server-truth
919
- // mode). 0.13.0: if `action` is omitted, this is LOCAL mode. The TUI renders
920
- // checkboxes from `selectedIds` (the server's pre-selection); clicks are
921
- // inert because the TUI doesn't track DOM-equivalent state across the
922
- // hook-less conformance walker. The browser carries the real local-mode
923
- // workflow; TUI is experimental — use the browser for interactive bulk
924
- // selection. The selection.buttons[] toolbar still renders below.
917
+ // Selection — leading [x]/[ ] column. TUI is render-only: checkboxes display
918
+ // selectedIds; clicks are inert (the TUI doesn't track DOM-equivalent state
919
+ // across the hook-less conformance walker). Bulk actions live in
920
+ // selection.buttons[]; the harvest reads sel.selectedIds (server's
921
+ // pre-selection). The browser carries the interactive surface.
925
922
  const sel = node.selection;
926
923
  const effectiveSet = sel ? new Set(sel.selectedIds) : null;
927
924
  const allOnPage = sel != null && node.rows.length > 0 &&
928
925
  node.rows.every((r) => r.id != null && effectiveSet.has(r.id));
929
- const onToggleAll = sel?.action
930
- ? () => ctx.onAction({
931
- name: sel.action.name,
932
- context: { ...(sel.action.context ?? {}), all: true, checked: !allOnPage },
933
- })
934
- : undefined;
935
- const onToggleRow = sel?.action
936
- ? (rowId, checked) => ctx.onAction({
937
- name: sel.action.name,
938
- context: { ...(sel.action.context ?? {}), id: rowId, checked },
939
- })
940
- : undefined;
941
926
  return (_jsx("scrollbox", { focused: focused, focusable: isPaneFocusable, borderColor: focused ? "#88aaff" : "#555555", focusedBorderColor: "#88aaff", flexGrow: 1, flexShrink: 1, children: _jsxs("box", { flexDirection: "column", children: [sel?.buttons && sel.buttons.length > 0 ? (_jsx("box", { flexDirection: "row", gap: 1, children: sel.buttons.map((btn, i) => {
942
927
  const harvestCtx = {
943
928
  ...ctx,
@@ -950,7 +935,7 @@ function TableView({ node, ctx }) {
950
935
  },
951
936
  };
952
937
  return _jsx(ButtonView, { node: btn, ctx: harvestCtx }, i);
953
- }) })) : null, _jsxs("box", { flexDirection: "row", gap: 2, children: [sel ? (_jsx("text", { attributes: 1 /* BOLD */, ...(onToggleAll ? { onMouseDown: onToggleAll } : {}), children: allOnPage ? "[x]" : "[ ]" })) : null, node.columns.map((c) => {
938
+ }) })) : null, _jsxs("box", { flexDirection: "row", gap: 2, children: [sel ? (_jsx("text", { attributes: 1 /* BOLD */, children: allOnPage ? "[x]" : "[ ]" })) : null, node.columns.map((c) => {
954
939
  const isSorted = node.sortColumn === c.key;
955
940
  const caret = isSorted ? (node.sortDirection === "desc" ? " ↓" : " ↑") : "";
956
941
  // B5 — only sortable headers respond to clicks (matches BrowserAdapter).
@@ -964,11 +949,7 @@ function TableView({ node, ctx }) {
964
949
  : undefined;
965
950
  return (_jsxs("box", { flexDirection: "row", gap: 2, ...(onRowClick ? { onMouseDown: onRowClick } : {}), children: [sel ? (() => {
966
951
  const isSel = row.id != null && effectiveSet.has(row.id);
967
- const rowId = row.id;
968
- const onBox = rowId != null && onToggleRow
969
- ? () => onToggleRow(rowId, !isSel)
970
- : undefined;
971
- return (_jsx("text", { fg: isSel ? "#88ff88" : "#888888", ...(onBox ? { onMouseDown: onBox } : {}), children: isSel ? "[x]" : "[ ]" }));
952
+ return (_jsx("text", { fg: isSel ? "#88ff88" : "#888888", children: isSel ? "[x]" : "[ ]" }));
972
953
  })() : null, node.columns.map((c) => {
973
954
  const cell = row.cells[c.key] ?? "";
974
955
  if (c.linkLabel != null && cell.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",