@ashley-shrok/viewmodel-shell 1.0.1 → 1.1.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
@@ -49,8 +49,14 @@ export declare class BrowserAdapter implements Adapter {
49
49
  * sortActions[col.key]; filter inputs are bound to filterBinds[col.key],
50
50
  * every keystroke writes, Enter dispatches filterAction; pagination
51
51
  * prev/next write the target page to paginationBind then dispatch
52
- * prevAction/nextAction. Per-row buttons are plain ButtonNodes. Selection
53
- * is no longer a framework concept. */
52
+ * prevAction/nextAction. Per-row controls (row.actions[]) are a mix of
53
+ * ButtonNode and CheckboxNode dispatched by entry.type. When row.action
54
+ * is set, the entire <tr> becomes clickable + keyboard-activatable
55
+ * (Enter / Space — Space preventDefaults page scroll) and exposes
56
+ * role="button", tabindex=0, and an aria-label derived from cell text;
57
+ * clicks on per-row controls or cell linkLabel anchors stopPropagation
58
+ * so they don't also fire row.action. Selection is no longer a framework
59
+ * concept. */
54
60
  private table;
55
61
  private copyButton;
56
62
  }
package/dist/browser.js CHANGED
@@ -626,8 +626,14 @@ export class BrowserAdapter {
626
626
  * sortActions[col.key]; filter inputs are bound to filterBinds[col.key],
627
627
  * every keystroke writes, Enter dispatches filterAction; pagination
628
628
  * prev/next write the target page to paginationBind then dispatch
629
- * prevAction/nextAction. Per-row buttons are plain ButtonNodes. Selection
630
- * is no longer a framework concept. */
629
+ * prevAction/nextAction. Per-row controls (row.actions[]) are a mix of
630
+ * ButtonNode and CheckboxNode dispatched by entry.type. When row.action
631
+ * is set, the entire <tr> becomes clickable + keyboard-activatable
632
+ * (Enter / Space — Space preventDefaults page scroll) and exposes
633
+ * role="button", tabindex=0, and an aria-label derived from cell text;
634
+ * clicks on per-row controls or cell linkLabel anchors stopPropagation
635
+ * so they don't also fire row.action. Selection is no longer a framework
636
+ * concept. */
631
637
  table(n, parent, on) {
632
638
  const wrapper = document.createElement("div");
633
639
  wrapper.className = "vms-table-wrapper";
@@ -708,9 +714,36 @@ export class BrowserAdapter {
708
714
  let rowClass = "vms-table__row";
709
715
  if (row.variant)
710
716
  rowClass += ` vms-table__row--${row.variant}`;
717
+ if (row.action)
718
+ rowClass += " vms-table__row--clickable";
711
719
  tr.className = rowClass;
712
720
  if (row.id)
713
721
  tr.dataset.id = row.id;
722
+ // row.action — click-anywhere + keyboard + ARIA. Per-row controls and
723
+ // cell linkLabel anchors stopPropagation below so they don't double-fire.
724
+ if (row.action) {
725
+ const rowActionName = row.action.name;
726
+ tr.tabIndex = 0;
727
+ tr.setAttribute("role", "button");
728
+ const labelParts = Object.values(row.cells)
729
+ .filter(v => v && v.trim())
730
+ .map(v => v.trim());
731
+ const ariaLabel = labelParts.length > 0
732
+ ? labelParts.join(" · ")
733
+ : (row.id ? `Row ${row.id}` : "");
734
+ if (ariaLabel)
735
+ tr.setAttribute("aria-label", ariaLabel);
736
+ tr.addEventListener("click", () => { on({ name: rowActionName }); });
737
+ tr.addEventListener("keydown", (e) => {
738
+ if (e.key === "Enter") {
739
+ on({ name: rowActionName });
740
+ }
741
+ else if (e.key === " " || e.key === "Spacebar") {
742
+ e.preventDefault(); // suppress page scroll
743
+ on({ name: rowActionName });
744
+ }
745
+ });
746
+ }
714
747
  n.columns.forEach(col => {
715
748
  const td = document.createElement("td");
716
749
  td.className = "vms-table__td";
@@ -724,6 +757,9 @@ export class BrowserAdapter {
724
757
  a.target = "_blank";
725
758
  a.rel = "noopener noreferrer";
726
759
  }
760
+ if (row.action) {
761
+ a.addEventListener("click", (e) => { e.stopPropagation(); });
762
+ }
727
763
  td.appendChild(a);
728
764
  }
729
765
  else {
@@ -731,12 +767,24 @@ export class BrowserAdapter {
731
767
  }
732
768
  tr.appendChild(td);
733
769
  });
734
- // Per-row buttons render as plain ButtonNodes in a trailing actions cell.
770
+ // Per-row interactive controls dispatch by entry.type so a CheckboxNode
771
+ // renders as a real <input type="checkbox"> rather than silently as an
772
+ // empty button. When row.action is set, swallow clicks on the actions td
773
+ // so toggling a per-row control doesn't ALSO fire the row action.
735
774
  if (row.actions && row.actions.length > 0) {
736
775
  const td = document.createElement("td");
737
776
  td.className = "vms-table__td vms-table__td--actions";
738
- for (const btn of row.actions)
739
- this.button(btn, td, on);
777
+ for (const entry of row.actions) {
778
+ if (entry.type === "button")
779
+ this.button(entry, td, on);
780
+ else if (entry.type === "checkbox")
781
+ this.checkbox(entry, td, on);
782
+ // forward-compatible: unknown entry types no-op (the closed TS union
783
+ // narrows; the runtime guard is for late-arriving wire shapes).
784
+ }
785
+ if (row.action) {
786
+ td.addEventListener("click", (e) => { e.stopPropagation(); });
787
+ }
740
788
  tr.appendChild(td);
741
789
  }
742
790
  tbody.appendChild(tr);
package/dist/index.d.ts CHANGED
@@ -237,10 +237,22 @@ export interface TableColumn {
237
237
  export interface TableRow {
238
238
  id?: string;
239
239
  cells: Record<string, string>;
240
- /** Per-row action buttons. Each is a full ButtonNode with its own unique
241
- * action name (e.g. `delete-row-42`, `close-ticket-42`)per-row identity
242
- * is encoded in the name, not as a separate context payload. */
243
- actions?: ButtonNode[];
240
+ /** Click-anywhere row dispatch primitive. When set, the renderer makes the
241
+ * entire row clickable AND keyboard-activatable (Enter / Space Space
242
+ * preventDefaults page scroll) AND exposes accessibility (role="button",
243
+ * tabindex=0, aria-label derived from cell text). Per-row identity is
244
+ * encoded in the action name (e.g. `select-ticket-42`) — no context field,
245
+ * consistent with the Phase 6 wire. Coexists with `actions[]`: clicking a
246
+ * per-row button, checkbox, or cell linkLabel anchor does NOT also fire
247
+ * `row.action` (the renderer stops propagation on those targets). */
248
+ action?: ActionEvent;
249
+ /** Per-row interactive controls rendered in a trailing actions cell. Each
250
+ * entry is either a ButtonNode (its own unique action name encodes per-row
251
+ * identity, e.g. `delete-row-42`) or a CheckboxNode (its own `bind` path
252
+ * per row). The renderer dispatches by `entry.type` so both types render
253
+ * correctly — a previous version typed this as `ButtonNode[]` and called
254
+ * the button renderer blindly, silently dropping non-button entries. */
255
+ actions?: (ButtonNode | CheckboxNode)[];
244
256
  variant?: string;
245
257
  }
246
258
  export interface TablePagination {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "1.0.1",
3
+ "version": "1.1.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",
@@ -605,6 +605,10 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
605
605
  .vms-table__row:not(:last-child) .vms-table__td { border-bottom: 1px solid var(--vms-border); }
606
606
  .vms-table__row--clickable { cursor: pointer; }
607
607
  .vms-table__row--clickable:hover { background: var(--vms-surface); }
608
+ .vms-table__row--clickable:focus-visible {
609
+ outline: 2px solid var(--vms-accent);
610
+ outline-offset: -2px;
611
+ }
608
612
  /* Row status variants — tints derive from theme vars (color-mix), so a
609
613
  custom :root theme recolors them automatically; no app CSS, no literals. */
610
614
  .vms-table__row--done { opacity: 0.6; }