@ashley-shrok/viewmodel-shell 1.6.0 → 1.7.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 CHANGED
@@ -52,7 +52,10 @@ export declare class BrowserAdapter implements Adapter {
52
52
  * every keystroke writes, Enter dispatches filterAction; pagination
53
53
  * prev/next write the target page to paginationBind then dispatch
54
54
  * prevAction/nextAction. Per-row controls (row.actions[]) are a mix of
55
- * ButtonNode and CheckboxNode dispatched by entry.type. When row.action
55
+ * ButtonNode and CheckboxNode; the renderer partitions them by entry.type
56
+ * CheckboxNodes render in a dedicated LEADING column (left, the data-grid
57
+ * selection convention), ButtonNodes in the TRAILING actions cell (right).
58
+ * When row.action
56
59
  * is set, the entire <tr> becomes clickable + keyboard-activatable
57
60
  * (Enter / Space — Space preventDefaults page scroll) and exposes
58
61
  * role="button", tabindex=0, and an aria-label derived from cell text;
package/dist/browser.js CHANGED
@@ -781,7 +781,10 @@ export class BrowserAdapter {
781
781
  * every keystroke writes, Enter dispatches filterAction; pagination
782
782
  * prev/next write the target page to paginationBind then dispatch
783
783
  * prevAction/nextAction. Per-row controls (row.actions[]) are a mix of
784
- * ButtonNode and CheckboxNode dispatched by entry.type. When row.action
784
+ * ButtonNode and CheckboxNode; the renderer partitions them by entry.type
785
+ * CheckboxNodes render in a dedicated LEADING column (left, the data-grid
786
+ * selection convention), ButtonNodes in the TRAILING actions cell (right).
787
+ * When row.action
785
788
  * is set, the entire <tr> becomes clickable + keyboard-activatable
786
789
  * (Enter / Space — Space preventDefaults page scroll) and exposes
787
790
  * role="button", tabindex=0, and an aria-label derived from cell text;
@@ -795,6 +798,17 @@ export class BrowserAdapter {
795
798
  table.className = "vms-table";
796
799
  const thead = document.createElement("thead");
797
800
  const headerRow = document.createElement("tr");
801
+ // Per-row checkboxes render in a dedicated LEADING column. If ANY row
802
+ // carries a checkbox, every row gets a leading select cell (empty when the
803
+ // row has none) and the header/filter rows get a matching leading <th>, so
804
+ // body cells stay column-aligned with their headers. (The trailing actions
805
+ // cell needs no header because it's the LAST column — a leading column does.)
806
+ const tableHasCheckboxes = n.rows.some(r => r.actions?.some(e => e.type === "checkbox") ?? false);
807
+ if (tableHasCheckboxes) {
808
+ const selTh = document.createElement("th");
809
+ selTh.className = "vms-table__th vms-table__th--select";
810
+ headerRow.appendChild(selTh);
811
+ }
798
812
  const sortIntent = (n.sortBind != null ? this.sa.read(n.sortBind) : null);
799
813
  const sortedCol = sortIntent?.column;
800
814
  const sortedDir = sortIntent?.direction;
@@ -832,6 +846,9 @@ export class BrowserAdapter {
832
846
  const filterAction = n.filterAction;
833
847
  const filterRow = document.createElement("tr");
834
848
  filterRow.className = "vms-table__filter-row";
849
+ if (tableHasCheckboxes) {
850
+ filterRow.appendChild(document.createElement("th"));
851
+ }
835
852
  n.columns.forEach(col => {
836
853
  const th = document.createElement("th");
837
854
  if (col.filterable) {
@@ -898,6 +915,22 @@ export class BrowserAdapter {
898
915
  }
899
916
  });
900
917
  }
918
+ // Leading select cell — holds this row's checkbox controls (empty when
919
+ // the row has none). Rendered for every row whenever the table has any
920
+ // checkboxes, so columns line up with the leading <th> added above. When
921
+ // row.action is set, swallow clicks so toggling doesn't fire the row action.
922
+ if (tableHasCheckboxes) {
923
+ const selTd = document.createElement("td");
924
+ selTd.className = "vms-table__td vms-table__td--select";
925
+ for (const entry of row.actions ?? []) {
926
+ if (entry.type === "checkbox")
927
+ this.checkbox(entry, selTd, on);
928
+ }
929
+ if (row.action) {
930
+ selTd.addEventListener("click", (e) => { e.stopPropagation(); });
931
+ }
932
+ tr.appendChild(selTd);
933
+ }
901
934
  n.columns.forEach(col => {
902
935
  const td = document.createElement("td");
903
936
  td.className = "vms-table__td";
@@ -921,21 +954,15 @@ export class BrowserAdapter {
921
954
  }
922
955
  tr.appendChild(td);
923
956
  });
924
- // Per-row interactive controlsdispatch by entry.type so a CheckboxNode
925
- // renders as a real <input type="checkbox"> rather than silently as an
926
- // empty button. When row.action is set, swallow clicks on the actions td
927
- // so toggling a per-row control doesn't ALSO fire the row action.
928
- if (row.actions && row.actions.length > 0) {
957
+ // Trailing actions cellper-row ButtonNodes only (checkboxes render in
958
+ // the leading select cell above). When row.action is set, swallow clicks
959
+ // on the actions td so pressing a button doesn't ALSO fire the row action.
960
+ const buttonEntries = (row.actions ?? []).filter((e) => e.type === "button");
961
+ if (buttonEntries.length > 0) {
929
962
  const td = document.createElement("td");
930
963
  td.className = "vms-table__td vms-table__td--actions";
931
- for (const entry of row.actions) {
932
- if (entry.type === "button")
933
- this.button(entry, td, on);
934
- else if (entry.type === "checkbox")
935
- this.checkbox(entry, td, on);
936
- // forward-compatible: unknown entry types no-op (the closed TS union
937
- // narrows; the runtime guard is for late-arriving wire shapes).
938
- }
964
+ for (const entry of buttonEntries)
965
+ this.button(entry, td, on);
939
966
  if (row.action) {
940
967
  td.addEventListener("click", (e) => { e.stopPropagation(); });
941
968
  }
package/dist/index.d.ts CHANGED
@@ -320,11 +320,12 @@ export interface TableRow {
320
320
  * per-row button, checkbox, or cell linkLabel anchor does NOT also fire
321
321
  * `row.action` (the renderer stops propagation on those targets). */
322
322
  action?: ActionEvent;
323
- /** Per-row interactive controls rendered in a trailing actions cell. Each
324
- * entry is either a ButtonNode (its own unique action name encodes per-row
325
- * identity, e.g. `delete-row-42`) or a CheckboxNode (its own `bind` path
326
- * per row). The renderer dispatches by `entry.type` so both types render
327
- * correctly a previous version typed this as `ButtonNode[]` and called
323
+ /** Per-row interactive controls. Each entry is either a ButtonNode (its own
324
+ * unique action name encodes per-row identity, e.g. `delete-row-42`) or a
325
+ * CheckboxNode (its own `bind` path per row). The renderer partitions by
326
+ * `entry.type`: CheckboxNodes render in a dedicated LEADING column (left
327
+ * the data-grid selection convention), ButtonNodes in the TRAILING actions
328
+ * cell (right). A previous version typed this as `ButtonNode[]` and called
328
329
  * the button renderer blindly, silently dropping non-button entries. */
329
330
  actions?: (ButtonNode | CheckboxNode)[];
330
331
  variant?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "1.6.0",
3
+ "version": "1.7.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",