@ashley-shrok/viewmodel-shell 0.11.0 → 0.13.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 +127 -0
- package/dist/index.d.ts +52 -0
- package/dist/tui.js +82 -27
- package/package.json +1 -1
- package/styles/default.css +35 -0
package/dist/browser.js
CHANGED
|
@@ -637,6 +637,43 @@ export class BrowserAdapter {
|
|
|
637
637
|
table.className = "vms-table";
|
|
638
638
|
const thead = document.createElement("thead");
|
|
639
639
|
const headerRow = document.createElement("tr");
|
|
640
|
+
// Selection: a leading checkbox column. The header box is a select-all over
|
|
641
|
+
// the rows CURRENTLY rendered (the current page) — never unloaded rows.
|
|
642
|
+
const sel = n.selection;
|
|
643
|
+
const selectedSet = sel ? new Set(sel.selectedIds) : null;
|
|
644
|
+
if (sel) {
|
|
645
|
+
const th = document.createElement("th");
|
|
646
|
+
th.className = "vms-table__th vms-table__th--select";
|
|
647
|
+
const allOnPage = n.rows.length > 0 && n.rows.every(r => r.id != null && selectedSet.has(r.id));
|
|
648
|
+
const someOnPage = n.rows.some(r => r.id != null && selectedSet.has(r.id));
|
|
649
|
+
const box = document.createElement("input");
|
|
650
|
+
box.type = "checkbox";
|
|
651
|
+
box.className = "vms-table__select vms-table__select--all";
|
|
652
|
+
box.checked = allOnPage;
|
|
653
|
+
box.indeterminate = someOnPage && !allOnPage;
|
|
654
|
+
box.addEventListener("change", () => {
|
|
655
|
+
const selAction = sel.action;
|
|
656
|
+
if (selAction) {
|
|
657
|
+
// Server-truth mode: dispatch; server re-renders with new selectedIds.
|
|
658
|
+
on({ name: selAction.name, context: { ...(selAction.context ?? {}), all: true, checked: box.checked } });
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// Local mode (0.13.0): toggle every row checkbox + class in this table
|
|
662
|
+
// to match the header. No dispatch — the dispatch guard can't drop anything.
|
|
663
|
+
const want = box.checked;
|
|
664
|
+
table.querySelectorAll("tbody input.vms-table__select").forEach(rowBox => {
|
|
665
|
+
if (rowBox.disabled)
|
|
666
|
+
return;
|
|
667
|
+
rowBox.checked = want;
|
|
668
|
+
const tr = rowBox.closest(".vms-table__row");
|
|
669
|
+
if (tr)
|
|
670
|
+
tr.classList.toggle("vms-table__row--selected", want);
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
th.appendChild(box);
|
|
675
|
+
headerRow.appendChild(th);
|
|
676
|
+
}
|
|
640
677
|
n.columns.forEach(col => {
|
|
641
678
|
const th = document.createElement("th");
|
|
642
679
|
const isSorted = col.key === n.sortColumn;
|
|
@@ -665,6 +702,8 @@ export class BrowserAdapter {
|
|
|
665
702
|
const filterAction = n.filterAction;
|
|
666
703
|
const filterRow = document.createElement("tr");
|
|
667
704
|
filterRow.className = "vms-table__filter-row";
|
|
705
|
+
if (sel)
|
|
706
|
+
filterRow.appendChild(document.createElement("th")); // align under the select column
|
|
668
707
|
n.columns.forEach(col => {
|
|
669
708
|
const th = document.createElement("th");
|
|
670
709
|
if (col.filterable) {
|
|
@@ -698,6 +737,9 @@ export class BrowserAdapter {
|
|
|
698
737
|
rowClass += ` vms-table__row--${row.variant}`;
|
|
699
738
|
if (row.action)
|
|
700
739
|
rowClass += " vms-table__row--clickable";
|
|
740
|
+
const isSelected = sel != null && row.id != null && selectedSet.has(row.id);
|
|
741
|
+
if (isSelected)
|
|
742
|
+
rowClass += " vms-table__row--selected";
|
|
701
743
|
tr.className = rowClass;
|
|
702
744
|
if (row.id)
|
|
703
745
|
tr.dataset.id = row.id;
|
|
@@ -705,6 +747,46 @@ export class BrowserAdapter {
|
|
|
705
747
|
const rowAction = row.action;
|
|
706
748
|
tr.addEventListener("click", () => on(rowAction));
|
|
707
749
|
}
|
|
750
|
+
if (sel) {
|
|
751
|
+
const td = document.createElement("td");
|
|
752
|
+
td.className = "vms-table__td vms-table__td--select";
|
|
753
|
+
// A click in the checkbox cell must not also fire the row's click action.
|
|
754
|
+
td.addEventListener("click", (e) => e.stopPropagation());
|
|
755
|
+
const box = document.createElement("input");
|
|
756
|
+
box.type = "checkbox";
|
|
757
|
+
box.className = "vms-table__select";
|
|
758
|
+
box.checked = isSelected;
|
|
759
|
+
if (row.id != null) {
|
|
760
|
+
const rowId = row.id;
|
|
761
|
+
// data-id is what selection.buttons[] harvest reads on click.
|
|
762
|
+
box.dataset.id = rowId;
|
|
763
|
+
box.addEventListener("change", () => {
|
|
764
|
+
const selAction = sel.action;
|
|
765
|
+
if (selAction) {
|
|
766
|
+
on({ name: selAction.name, context: { ...(selAction.context ?? {}), id: rowId, checked: box.checked } });
|
|
767
|
+
}
|
|
768
|
+
else {
|
|
769
|
+
// Local mode (0.13.0): flip the row class to mirror the box, then
|
|
770
|
+
// reconcile the header select-all (could now be all / some / none).
|
|
771
|
+
tr.classList.toggle("vms-table__row--selected", box.checked);
|
|
772
|
+
const headerBox = table.querySelector("thead input.vms-table__select--all");
|
|
773
|
+
if (headerBox) {
|
|
774
|
+
const all = table.querySelectorAll("tbody input.vms-table__select:not(:disabled)");
|
|
775
|
+
let checked = 0;
|
|
776
|
+
all.forEach(b => { if (b.checked)
|
|
777
|
+
checked++; });
|
|
778
|
+
headerBox.checked = all.length > 0 && checked === all.length;
|
|
779
|
+
headerBox.indeterminate = checked > 0 && checked < all.length;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
box.disabled = true; // selection addresses rows by id; a row without one can't be selected
|
|
786
|
+
}
|
|
787
|
+
td.appendChild(box);
|
|
788
|
+
tr.appendChild(td);
|
|
789
|
+
}
|
|
708
790
|
n.columns.forEach(col => {
|
|
709
791
|
const td = document.createElement("td");
|
|
710
792
|
td.className = "vms-table__td";
|
|
@@ -729,6 +811,51 @@ export class BrowserAdapter {
|
|
|
729
811
|
});
|
|
730
812
|
table.appendChild(tbody);
|
|
731
813
|
wrapper.appendChild(table);
|
|
814
|
+
// 0.13.0 — bulk-action toolbar rendered ABOVE the table. Each button's
|
|
815
|
+
// click harvests the table's currently-checked row ids and merges them as
|
|
816
|
+
// `selectedIds` into the action's context. Works with both server-truth
|
|
817
|
+
// mode (the DOM mirrors selectedIds so the harvest matches state) and
|
|
818
|
+
// local mode (the DOM is the only source of selection truth).
|
|
819
|
+
if (sel?.buttons && sel.buttons.length > 0) {
|
|
820
|
+
const toolbar = document.createElement("div");
|
|
821
|
+
toolbar.className = "vms-table__bulk-actions";
|
|
822
|
+
const harvest = (action) => {
|
|
823
|
+
const ids = [];
|
|
824
|
+
table.querySelectorAll("tbody input.vms-table__select:checked").forEach(b => {
|
|
825
|
+
if (b.dataset.id)
|
|
826
|
+
ids.push(b.dataset.id);
|
|
827
|
+
});
|
|
828
|
+
on({ name: action.name, context: { ...(action.context ?? {}), selectedIds: ids } });
|
|
829
|
+
};
|
|
830
|
+
sel.buttons.forEach(btn => this.button(btn, toolbar, harvest));
|
|
831
|
+
wrapper.insertBefore(toolbar, table);
|
|
832
|
+
}
|
|
833
|
+
if (n.pagination) {
|
|
834
|
+
const pg = n.pagination;
|
|
835
|
+
const footer = document.createElement("div");
|
|
836
|
+
footer.className = "vms-table__pagination";
|
|
837
|
+
const totalPages = Math.max(1, Math.ceil(pg.totalRows / pg.pageSize));
|
|
838
|
+
const from = pg.totalRows === 0 ? 0 : (pg.page - 1) * pg.pageSize + 1;
|
|
839
|
+
const to = Math.min(pg.page * pg.pageSize, pg.totalRows);
|
|
840
|
+
const range = document.createElement("span");
|
|
841
|
+
range.className = "vms-table__pagination-range";
|
|
842
|
+
range.textContent = `${from}–${to} of ${pg.totalRows}`;
|
|
843
|
+
footer.appendChild(range);
|
|
844
|
+
const pgAction = pg.action;
|
|
845
|
+
const mkBtn = (label, targetPage, disabled) => {
|
|
846
|
+
const b = document.createElement("button");
|
|
847
|
+
b.type = "button";
|
|
848
|
+
b.className = "vms-button vms-button--secondary vms-table__pagination-btn";
|
|
849
|
+
b.textContent = label;
|
|
850
|
+
b.disabled = disabled;
|
|
851
|
+
if (!disabled)
|
|
852
|
+
b.addEventListener("click", () => on({ name: pgAction.name, context: { ...(pgAction.context ?? {}), page: targetPage } }));
|
|
853
|
+
return b;
|
|
854
|
+
};
|
|
855
|
+
footer.appendChild(mkBtn("‹ Prev", pg.page - 1, pg.page <= 1));
|
|
856
|
+
footer.appendChild(mkBtn("Next ›", pg.page + 1, pg.page >= totalPages));
|
|
857
|
+
wrapper.appendChild(footer);
|
|
858
|
+
}
|
|
732
859
|
parent.appendChild(wrapper);
|
|
733
860
|
}
|
|
734
861
|
copyButton(n, parent) {
|
package/dist/index.d.ts
CHANGED
|
@@ -202,6 +202,46 @@ export interface TableRow {
|
|
|
202
202
|
action?: ActionEvent;
|
|
203
203
|
variant?: string;
|
|
204
204
|
}
|
|
205
|
+
export interface TableSelection {
|
|
206
|
+
/** Row ids that should render PRE-SELECTED on this render. In server-truth
|
|
207
|
+
* mode (`action` set) this is the live selection, round-tripped in state and
|
|
208
|
+
* authoritative every render. In local mode (`action` omitted) this is the
|
|
209
|
+
* server's initial pre-selection only — subsequent toggles are purely
|
|
210
|
+
* client-side DOM state and the server doesn't see them until a `buttons[]`
|
|
211
|
+
* click harvests them. */
|
|
212
|
+
selectedIds: string[];
|
|
213
|
+
/** OPTIONAL (0.13.0). When present: server-truth mode — every checkbox toggle
|
|
214
|
+
* dispatches this action with merged `{ id, checked }` per row or
|
|
215
|
+
* `{ all: true, checked }` for the header select-all (where "all" = the
|
|
216
|
+
* rendered page). Selection survives sort/filter/pagination via the state
|
|
217
|
+
* round-trip. When OMITTED: local mode — the adapter toggles the DOM
|
|
218
|
+
* checkbox + `.vms-table__row--selected` class purely client-side with no
|
|
219
|
+
* dispatch. Local mode is the recommended pattern for rapid-selection
|
|
220
|
+
* workflows (no dropped clicks under the dispatch guard); see `buttons` for
|
|
221
|
+
* how bulk actions read the resulting selection. */
|
|
222
|
+
action?: ActionEvent;
|
|
223
|
+
/** OPTIONAL (0.13.0). When present, the adapter renders these as a bulk-action
|
|
224
|
+
* toolbar ABOVE the table. On click, each button harvests the currently
|
|
225
|
+
* checked rows from the DOM and dispatches its `action` with
|
|
226
|
+
* `{ selectedIds: [...] }` merged into its `context`. Designed primarily to
|
|
227
|
+
* pair with local mode (`action` absent) — it's how the server learns the
|
|
228
|
+
* selection without a per-toggle round-trip — but works in server-truth mode
|
|
229
|
+
* too (the harvest matches `selectedIds` since the DOM reflects server-truth
|
|
230
|
+
* after each render). */
|
|
231
|
+
buttons?: ButtonNode[];
|
|
232
|
+
}
|
|
233
|
+
export interface TablePagination {
|
|
234
|
+
/** 1-based current page. */
|
|
235
|
+
page: number;
|
|
236
|
+
/** Rows per page. Drives the "X–Y of N" range label and the last-page calc. */
|
|
237
|
+
pageSize: number;
|
|
238
|
+
/** Total rows across all pages — server-truth. The adapter renders the range
|
|
239
|
+
* label and enables/disables prev/next from this; it does NOT slice. */
|
|
240
|
+
totalRows: number;
|
|
241
|
+
/** Dispatched on a page-control click. The adapter merges `{ page }` — the
|
|
242
|
+
* target 1-based page. */
|
|
243
|
+
action: ActionEvent;
|
|
244
|
+
}
|
|
205
245
|
export interface TableNode {
|
|
206
246
|
type: "table";
|
|
207
247
|
columns: TableColumn[];
|
|
@@ -212,6 +252,18 @@ export interface TableNode {
|
|
|
212
252
|
sortAction?: ActionEvent;
|
|
213
253
|
/** Base action. Adapter merges { column, value, filters } into context on Enter. */
|
|
214
254
|
filterAction?: ActionEvent;
|
|
255
|
+
/** Per-row multi-select. When set, the adapter renders a leading checkbox
|
|
256
|
+
* column + a header select-all checkbox and tints selected rows. `TableRow.id`
|
|
257
|
+
* is REQUIRED on every row when selection is set — it's the address the
|
|
258
|
+
* toggle action reports back. */
|
|
259
|
+
selection?: TableSelection;
|
|
260
|
+
/** Server-driven pagination. When set, the adapter renders an "X–Y of N"
|
|
261
|
+
* range + prev/next controls below the table. **The server slices `rows` to
|
|
262
|
+
* the current page** — the adapter never paginates client-side (that would
|
|
263
|
+
* break for DB-backed tables, which are most of them). By convention
|
|
264
|
+
* `sortAction` / `filterAction` reset `page` to 1 on the server side, since
|
|
265
|
+
* the row window changes underneath them. */
|
|
266
|
+
pagination?: TablePagination;
|
|
215
267
|
}
|
|
216
268
|
export interface CopyButtonNode {
|
|
217
269
|
type: "copy-button";
|
package/dist/tui.js
CHANGED
|
@@ -914,38 +914,93 @@ function TableView({ node, ctx }) {
|
|
|
914
914
|
context: { ...(node.sortAction.context ?? {}), column: columnKey, direction },
|
|
915
915
|
});
|
|
916
916
|
};
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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.
|
|
925
|
+
const sel = node.selection;
|
|
926
|
+
const effectiveSet = sel ? new Set(sel.selectedIds) : null;
|
|
927
|
+
const allOnPage = sel != null && node.rows.length > 0 &&
|
|
928
|
+
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
|
+
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
|
+
const harvestCtx = {
|
|
943
|
+
...ctx,
|
|
944
|
+
onAction: (action) => {
|
|
945
|
+
const ids = [...effectiveSet];
|
|
946
|
+
ctx.onAction({
|
|
947
|
+
name: action.name,
|
|
948
|
+
context: { ...(action.context ?? {}), selectedIds: ids },
|
|
949
|
+
});
|
|
950
|
+
},
|
|
951
|
+
};
|
|
952
|
+
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) => {
|
|
954
|
+
const isSorted = node.sortColumn === c.key;
|
|
955
|
+
const caret = isSorted ? (node.sortDirection === "desc" ? " ↓" : " ↑") : "";
|
|
956
|
+
// B5 — only sortable headers respond to clicks (matches BrowserAdapter).
|
|
957
|
+
const clickable = c.sortable && node.sortAction != null;
|
|
958
|
+
const onMouseDown = clickable ? () => onHeaderClick(c.key) : undefined;
|
|
959
|
+
return (_jsxs("text", { attributes: 1 /* BOLD */, ...(onMouseDown ? { onMouseDown } : {}), children: [c.label, caret] }, c.key));
|
|
960
|
+
})] }), node.columns.some((c) => c.filterable) ? (_jsxs("box", { flexDirection: "row", gap: 2, children: [sel ? _jsx("text", { fg: "#888888", children: " " }) : null, node.columns.map((c) => (_jsx("text", { fg: "#888888", children: c.filterable ? (c.filterValue ? `[${c.filterValue}]` : "[filter]") : "" }, c.key)))] })) : null, node.rows.map((row, ri) => {
|
|
925
961
|
// B5 — row click dispatches row.action when present.
|
|
926
962
|
const onRowClick = row.action
|
|
927
963
|
? () => ctx.onAction(row.action)
|
|
928
964
|
: undefined;
|
|
929
|
-
return (
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
// natively via the terminal's OSC-8 support; internal cell
|
|
935
|
-
// links route through navigate (B5).
|
|
936
|
-
const ESC = String.fromCharCode(27);
|
|
937
|
-
const BEL = String.fromCharCode(7);
|
|
938
|
-
const inner = c.linkExternal
|
|
939
|
-
? `${ESC}]8;;${cell}${BEL}${c.linkLabel}${ESC}]8;;${BEL}`
|
|
940
|
-
: c.linkLabel;
|
|
941
|
-
const cellClick = !c.linkExternal && cell.length > 0
|
|
942
|
-
? () => ctx.navigate(cell)
|
|
965
|
+
return (_jsxs("box", { flexDirection: "row", gap: 2, ...(onRowClick ? { onMouseDown: onRowClick } : {}), children: [sel ? (() => {
|
|
966
|
+
const isSel = row.id != null && effectiveSet.has(row.id);
|
|
967
|
+
const rowId = row.id;
|
|
968
|
+
const onBox = rowId != null && onToggleRow
|
|
969
|
+
? () => onToggleRow(rowId, !isSel)
|
|
943
970
|
: undefined;
|
|
944
|
-
return (_jsx("text", {
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
971
|
+
return (_jsx("text", { fg: isSel ? "#88ff88" : "#888888", ...(onBox ? { onMouseDown: onBox } : {}), children: isSel ? "[x]" : "[ ]" }));
|
|
972
|
+
})() : null, node.columns.map((c) => {
|
|
973
|
+
const cell = row.cells[c.key] ?? "";
|
|
974
|
+
if (c.linkLabel != null && cell.length > 0) {
|
|
975
|
+
// Cell is a link — emit OSC-8 for external links, plain
|
|
976
|
+
// underlined text otherwise. External links are clickable
|
|
977
|
+
// natively via the terminal's OSC-8 support; internal cell
|
|
978
|
+
// links route through navigate (B5).
|
|
979
|
+
const ESC = String.fromCharCode(27);
|
|
980
|
+
const BEL = String.fromCharCode(7);
|
|
981
|
+
const inner = c.linkExternal
|
|
982
|
+
? `${ESC}]8;;${cell}${BEL}${c.linkLabel}${ESC}]8;;${BEL}`
|
|
983
|
+
: c.linkLabel;
|
|
984
|
+
const cellClick = !c.linkExternal && cell.length > 0
|
|
985
|
+
? () => ctx.navigate(cell)
|
|
986
|
+
: undefined;
|
|
987
|
+
return (_jsx("text", { attributes: 4 /* UNDERLINE */, fg: "#6688cc", ...(cellClick ? { onMouseDown: cellClick } : {}), children: inner }, c.key));
|
|
988
|
+
}
|
|
989
|
+
return _jsx("text", { children: cell }, c.key);
|
|
990
|
+
})] }, row.id ?? ri));
|
|
991
|
+
}), node.pagination ? (() => {
|
|
992
|
+
const pg = node.pagination;
|
|
993
|
+
const totalPages = Math.max(1, Math.ceil(pg.totalRows / pg.pageSize));
|
|
994
|
+
const from = pg.totalRows === 0 ? 0 : (pg.page - 1) * pg.pageSize + 1;
|
|
995
|
+
const to = Math.min(pg.page * pg.pageSize, pg.totalRows);
|
|
996
|
+
const go = (p) => ctx.onAction({
|
|
997
|
+
name: pg.action.name,
|
|
998
|
+
context: { ...(pg.action.context ?? {}), page: p },
|
|
999
|
+
});
|
|
1000
|
+
const canPrev = pg.page > 1;
|
|
1001
|
+
const canNext = pg.page < totalPages;
|
|
1002
|
+
return (_jsxs("box", { flexDirection: "row", gap: 2, children: [_jsx("text", { fg: "#888888", children: `${from}–${to} of ${pg.totalRows}` }), _jsx("text", { fg: canPrev ? "#88aaff" : "#555555", ...(canPrev ? { onMouseDown: () => go(pg.page - 1) } : {}), children: "‹ Prev" }), _jsx("text", { fg: canNext ? "#88aaff" : "#555555", ...(canNext ? { onMouseDown: () => go(pg.page + 1) } : {}), children: "Next ›" })] }));
|
|
1003
|
+
})() : null] }) }));
|
|
949
1004
|
}
|
|
950
1005
|
// ── minimum-viable text surface for the rest of the node set ───────────────
|
|
951
1006
|
// Same as B1 — text only, no interactivity. Full widgets land in B3/B4.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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
|
@@ -614,6 +614,41 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
614
614
|
.vms-table__link { color: var(--vms-accent); text-decoration: none; }
|
|
615
615
|
.vms-table__link:hover { text-decoration: underline; }
|
|
616
616
|
|
|
617
|
+
/* Selection — leading checkbox column + selected-row tint. The tint derives
|
|
618
|
+
from --vms-accent via color-mix, so a custom :root theme recolors it
|
|
619
|
+
automatically (same pattern as the status variants above). Selected wins
|
|
620
|
+
over the clickable-hover background so a selected row stays legible. */
|
|
621
|
+
.vms-table__th--select,
|
|
622
|
+
.vms-table__td--select { width: 1%; white-space: nowrap; text-align: center; padding-right: 0; }
|
|
623
|
+
.vms-table__select { accent-color: var(--vms-accent); cursor: pointer; }
|
|
624
|
+
.vms-table__row--selected { background: color-mix(in srgb, var(--vms-accent) 12%, transparent); }
|
|
625
|
+
.vms-table__row--selected.vms-table__row--clickable:hover { background: color-mix(in srgb, var(--vms-accent) 18%, transparent); }
|
|
626
|
+
|
|
627
|
+
/* 0.13.0 — bulk-action toolbar rendered ABOVE the table (when
|
|
628
|
+
TableNode.selection.buttons[] is set). Reuses .vms-button styling; only
|
|
629
|
+
layout is added — flex row, wrap on narrow, gap from the spacing rhythm. */
|
|
630
|
+
.vms-table__bulk-actions {
|
|
631
|
+
display: flex;
|
|
632
|
+
flex-direction: row;
|
|
633
|
+
flex-wrap: wrap;
|
|
634
|
+
gap: var(--vms-space-sm);
|
|
635
|
+
padding-bottom: var(--vms-space-sm);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/* Pagination footer — range label + prev/next, separated from the table by a
|
|
639
|
+
hairline. Buttons reuse .vms-button styling; only sizing/disabled is added. */
|
|
640
|
+
.vms-table__pagination {
|
|
641
|
+
display: flex;
|
|
642
|
+
align-items: center;
|
|
643
|
+
gap: var(--vms-space-sm);
|
|
644
|
+
padding-top: var(--vms-space-sm);
|
|
645
|
+
margin-top: var(--vms-space-xs);
|
|
646
|
+
border-top: 1px solid var(--vms-border);
|
|
647
|
+
}
|
|
648
|
+
.vms-table__pagination-range { color: var(--vms-text-muted); font-size: var(--vms-text-sm); margin-right: auto; }
|
|
649
|
+
.vms-table__pagination-btn { padding: var(--vms-space-xs) var(--vms-space-sm); font-size: var(--vms-text-sm); }
|
|
650
|
+
.vms-table__pagination-btn:disabled { opacity: 0.45; cursor: default; }
|
|
651
|
+
|
|
617
652
|
/* ── Recommended error-banner pattern ──
|
|
618
653
|
Apps typically render this from the `onError` callback. Not emitted by
|
|
619
654
|
the framework; included here so apps can use the convention without
|