@ashley-shrok/viewmodel-shell 0.14.0 → 0.16.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 +5 -0
- package/dist/browser.js +31 -35
- package/dist/index.d.ts +29 -24
- package/dist/index.js +32 -2
- package/dist/server.d.ts +6 -0
- package/dist/tui.js +7 -26
- package/package.json +1 -1
- package/styles/default.css +12 -0
package/dist/browser.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
10
10
|
* contentType is informational — the Blob's own .type takes precedence in
|
|
11
11
|
* browsers. We accept the arg for adapter symmetry (other adapters use it). */
|
|
12
12
|
saveFile(data: Blob, filename: string, _contentType: string): void;
|
|
13
|
+
/** 0.16.0 — toggle the `.vms-busy` class on the container. Default CSS makes
|
|
14
|
+
* every interactive descendant non-clickable (cursor:wait + pointer-events:
|
|
15
|
+
* none), so a rapid checkbox click during a round-trip never visually flips
|
|
16
|
+
* the box. Idempotent (classList.toggle with a force value). */
|
|
17
|
+
setBusy(active: boolean): void;
|
|
13
18
|
/** 0.14.0 — install / clear the browser's `beforeunload` guard. The shell
|
|
14
19
|
* calls this on every response with `response.preventUnload ?? false`, so
|
|
15
20
|
* the lock-and-clear cycle is just "server sets preventUnload:true while
|
package/dist/browser.js
CHANGED
|
@@ -105,6 +105,13 @@ export class BrowserAdapter {
|
|
|
105
105
|
setTimeout(() => URL.revokeObjectURL(url), 0);
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
|
+
/** 0.16.0 — toggle the `.vms-busy` class on the container. Default CSS makes
|
|
109
|
+
* every interactive descendant non-clickable (cursor:wait + pointer-events:
|
|
110
|
+
* none), so a rapid checkbox click during a round-trip never visually flips
|
|
111
|
+
* the box. Idempotent (classList.toggle with a force value). */
|
|
112
|
+
setBusy(active) {
|
|
113
|
+
this.container.classList.toggle("vms-busy", active);
|
|
114
|
+
}
|
|
108
115
|
/** 0.14.0 — install / clear the browser's `beforeunload` guard. The shell
|
|
109
116
|
* calls this on every response with `response.preventUnload ?? false`, so
|
|
110
117
|
* the lock-and-clear cycle is just "server sets preventUnload:true while
|
|
@@ -674,24 +681,18 @@ export class BrowserAdapter {
|
|
|
674
681
|
box.checked = allOnPage;
|
|
675
682
|
box.indeterminate = someOnPage && !allOnPage;
|
|
676
683
|
box.addEventListener("change", () => {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
}
|
|
684
|
+
// Toggle every row checkbox + class in this table to match the header.
|
|
685
|
+
// No dispatch — selection is purely DOM-local; bulk actions harvest the
|
|
686
|
+
// checked rows when a `selection.buttons[]` entry is clicked.
|
|
687
|
+
const want = box.checked;
|
|
688
|
+
table.querySelectorAll("tbody input.vms-table__select").forEach(rowBox => {
|
|
689
|
+
if (rowBox.disabled)
|
|
690
|
+
return;
|
|
691
|
+
rowBox.checked = want;
|
|
692
|
+
const tr = rowBox.closest(".vms-table__row");
|
|
693
|
+
if (tr)
|
|
694
|
+
tr.classList.toggle("vms-table__row--selected", want);
|
|
695
|
+
});
|
|
695
696
|
});
|
|
696
697
|
th.appendChild(box);
|
|
697
698
|
headerRow.appendChild(th);
|
|
@@ -783,23 +784,18 @@ export class BrowserAdapter {
|
|
|
783
784
|
// data-id is what selection.buttons[] harvest reads on click.
|
|
784
785
|
box.dataset.id = rowId;
|
|
785
786
|
box.addEventListener("change", () => {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
}
|
|
787
|
+
// Flip the row class to mirror the box, then reconcile the header
|
|
788
|
+
// select-all (could now be all / some / none). No dispatch — bulk
|
|
789
|
+
// actions read the DOM via the selection.buttons[] harvest.
|
|
790
|
+
tr.classList.toggle("vms-table__row--selected", box.checked);
|
|
791
|
+
const headerBox = table.querySelector("thead input.vms-table__select--all");
|
|
792
|
+
if (headerBox) {
|
|
793
|
+
const all = table.querySelectorAll("tbody input.vms-table__select:not(:disabled)");
|
|
794
|
+
let checked = 0;
|
|
795
|
+
all.forEach(b => { if (b.checked)
|
|
796
|
+
checked++; });
|
|
797
|
+
headerBox.checked = all.length > 0 && checked === all.length;
|
|
798
|
+
headerBox.indeterminate = checked > 0 && checked < all.length;
|
|
803
799
|
}
|
|
804
800
|
});
|
|
805
801
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -42,6 +42,16 @@ export interface Adapter {
|
|
|
42
42
|
* targets (TUI) have no terminal equivalent. Modern browsers show a
|
|
43
43
|
* generic "Leave site?" dialog; the message is not customizable. */
|
|
44
44
|
setPreventUnload?(active: boolean): void;
|
|
45
|
+
/** 0.16.0 — visually lock the UI during periods where user dispatches will
|
|
46
|
+
* be dropped. Called by the shell on every transition with `active = true`
|
|
47
|
+
* when EITHER a user-initiated dispatch is in flight OR the server returned
|
|
48
|
+
* `ShellResponse.busy: true` on its most recent response. Polls (silent
|
|
49
|
+
* dispatches) bypass — they're the only way out of a server-busy state.
|
|
50
|
+
* `BrowserAdapter` toggles `.vms-busy` on its container; default CSS makes
|
|
51
|
+
* every interactive descendant non-clickable (cursor: wait + pointer-events:
|
|
52
|
+
* none), so a rapid checkbox click during an in-flight round-trip never
|
|
53
|
+
* visually flips the box. Fail-quiet by absence (TUI has no equivalent). */
|
|
54
|
+
setBusy?(active: boolean): void;
|
|
45
55
|
}
|
|
46
56
|
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode;
|
|
47
57
|
export interface PageNode {
|
|
@@ -213,31 +223,19 @@ export interface TableRow {
|
|
|
213
223
|
variant?: string;
|
|
214
224
|
}
|
|
215
225
|
export interface TableSelection {
|
|
216
|
-
/** Row ids that should render PRE-SELECTED on
|
|
217
|
-
*
|
|
218
|
-
*
|
|
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. */
|
|
226
|
+
/** Row ids that should render PRE-SELECTED on initial render — the server's
|
|
227
|
+
* initial pre-selection. Subsequent toggles are purely client-side DOM state;
|
|
228
|
+
* the server doesn't see them until a `buttons[]` click harvests them. */
|
|
222
229
|
selectedIds: string[];
|
|
223
|
-
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
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). */
|
|
230
|
+
/** When present, the adapter renders these as a bulk-action toolbar ABOVE
|
|
231
|
+
* the table. On click, each button harvests the currently-checked rows from
|
|
232
|
+
* the DOM and dispatches its `action` with `{ selectedIds: [...] }` merged
|
|
233
|
+
* into its `context`. This is the only way to act on the selection — there
|
|
234
|
+
* is no per-toggle dispatch (0.15.0 removed the `action` mode that did,
|
|
235
|
+
* because rapid clicks were dropped by the dispatch guard and the in-flight
|
|
236
|
+
* response wiped the DOM). If a future release needs cross-page persistence
|
|
237
|
+
* or live "N selected" indicators, the way back is a redesigned wire shape
|
|
238
|
+
* (dispatch queueing + optimistic preservation), not the old `action` mode. */
|
|
241
239
|
buttons?: ButtonNode[];
|
|
242
240
|
}
|
|
243
241
|
export interface TablePagination {
|
|
@@ -336,6 +334,10 @@ export interface ShellResponse {
|
|
|
336
334
|
* `preventUnload: true` from each response; clear it when the work
|
|
337
335
|
* completes (typically via polling). See `Adapter.setPreventUnload`. */
|
|
338
336
|
preventUnload?: boolean;
|
|
337
|
+
/** 0.16.0 — when true, the shell locks the UI (drops user-initiated
|
|
338
|
+
* dispatches; the adapter applies `.vms-busy` for the visual cue). Polls
|
|
339
|
+
* bypass so the server can clear the state. See `Adapter.setBusy`. */
|
|
340
|
+
busy?: boolean;
|
|
339
341
|
}
|
|
340
342
|
export declare class ViewModelShell {
|
|
341
343
|
private options;
|
|
@@ -343,7 +345,10 @@ export declare class ViewModelShell {
|
|
|
343
345
|
private currentState;
|
|
344
346
|
private dispatching;
|
|
345
347
|
private pollTimer;
|
|
348
|
+
private serverBusy;
|
|
349
|
+
private userDispatching;
|
|
346
350
|
constructor(options: ShellOptions);
|
|
351
|
+
private syncBusy;
|
|
347
352
|
load(params?: Record<string, string>): Promise<void>;
|
|
348
353
|
dispatch(action: ActionEvent, silent?: boolean): Promise<void>;
|
|
349
354
|
/** Feed a pre-parsed ShellResponse into the shell — for SSE/WebSocket integrations. */
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,18 @@ export class ViewModelShell {
|
|
|
5
5
|
currentState = null;
|
|
6
6
|
dispatching = false;
|
|
7
7
|
pollTimer = null;
|
|
8
|
+
// 0.16.0 — busy = serverBusy OR a user-initiated dispatch is in flight.
|
|
9
|
+
// Polls (silent=true dispatches) don't flip userDispatching so they never
|
|
10
|
+
// toggle the busy class — that's how a server-busy state stays continuously
|
|
11
|
+
// locked across many ticks without flicker.
|
|
12
|
+
serverBusy = false;
|
|
13
|
+
userDispatching = false;
|
|
8
14
|
constructor(options) {
|
|
9
15
|
this.options = options;
|
|
10
16
|
}
|
|
17
|
+
syncBusy() {
|
|
18
|
+
this.options.adapter.setBusy?.(this.serverBusy || this.userDispatching);
|
|
19
|
+
}
|
|
11
20
|
async load(params) {
|
|
12
21
|
const { endpoint, adapter, onError, onLoading } = this.options;
|
|
13
22
|
this.stopPolling();
|
|
@@ -25,6 +34,9 @@ export class ViewModelShell {
|
|
|
25
34
|
// server may legitimately want it on at first paint (e.g. the page was
|
|
26
35
|
// refreshed mid-work and the long action is still pending server-side).
|
|
27
36
|
adapter.setPreventUnload?.(body.preventUnload ?? false);
|
|
37
|
+
// 0.16.0 — same for the busy lockout.
|
|
38
|
+
this.serverBusy = body.busy ?? false;
|
|
39
|
+
this.syncBusy();
|
|
28
40
|
adapter.render(body.vm, (action) => this.dispatch(action));
|
|
29
41
|
this.schedulePoll(body.nextPollIn);
|
|
30
42
|
}
|
|
@@ -37,6 +49,10 @@ export class ViewModelShell {
|
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
async dispatch(action, silent = false) {
|
|
52
|
+
// 0.16.0 — drop user-initiated dispatches while server-busy. Polls (silent)
|
|
53
|
+
// bypass so the server can clear the busy state.
|
|
54
|
+
if (!silent && this.serverBusy)
|
|
55
|
+
return;
|
|
40
56
|
if (this.dispatching)
|
|
41
57
|
return;
|
|
42
58
|
const { actionEndpoint, onError, onLoading } = this.options;
|
|
@@ -48,8 +64,16 @@ export class ViewModelShell {
|
|
|
48
64
|
}
|
|
49
65
|
try {
|
|
50
66
|
this.dispatching = true;
|
|
51
|
-
if (!silent)
|
|
67
|
+
if (!silent) {
|
|
68
|
+
// 0.16.0 — flag a user dispatch as in-flight + apply .vms-busy. This
|
|
69
|
+
// is what kills the "rapid clicks during a round-trip silently flip the
|
|
70
|
+
// checkbox" UX bug: by the time the user's second click arrives, the
|
|
71
|
+
// container has pointer-events: none and the click never reaches the
|
|
72
|
+
// input.
|
|
73
|
+
this.userDispatching = true;
|
|
74
|
+
this.syncBusy();
|
|
52
75
|
onLoading?.(true);
|
|
76
|
+
}
|
|
53
77
|
const form = new FormData();
|
|
54
78
|
form.append("_action", JSON.stringify({ name: action.name, context: action.context ?? {} }));
|
|
55
79
|
form.append("_state", JSON.stringify(this.currentState));
|
|
@@ -93,8 +117,11 @@ export class ViewModelShell {
|
|
|
93
117
|
}
|
|
94
118
|
finally {
|
|
95
119
|
this.dispatching = false;
|
|
96
|
-
if (!silent)
|
|
120
|
+
if (!silent) {
|
|
121
|
+
this.userDispatching = false;
|
|
122
|
+
this.syncBusy();
|
|
97
123
|
onLoading?.(false);
|
|
124
|
+
}
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
/** Feed a pre-parsed ShellResponse into the shell — for SSE/WebSocket integrations. */
|
|
@@ -147,6 +174,9 @@ export class ViewModelShell {
|
|
|
147
174
|
// wants a redirect to NOT be blocked by its own guard simply omits
|
|
148
175
|
// preventUnload (or sets it false) on that response — standard pattern.
|
|
149
176
|
adapter.setPreventUnload?.(body.preventUnload ?? false);
|
|
177
|
+
// 0.16.0 — likewise for the busy lockout.
|
|
178
|
+
this.serverBusy = body.busy ?? false;
|
|
179
|
+
this.syncBusy();
|
|
150
180
|
if (body.redirect) {
|
|
151
181
|
if (this.options.onRedirect) {
|
|
152
182
|
this.options.onRedirect(body.redirect);
|
package/dist/server.d.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface ShellResponseBody<TState> {
|
|
|
22
22
|
* (or set false) to clear; set true while a long-running server action is
|
|
23
23
|
* in flight so an accidental tab-close doesn't lose work. */
|
|
24
24
|
preventUnload?: boolean;
|
|
25
|
+
/** 0.16.0 — lock the UI: the shell drops user-initiated dispatches client-
|
|
26
|
+
* side and the BrowserAdapter applies `.vms-busy` (cursor:wait + pointer-
|
|
27
|
+
* events:none on interactive descendants). Polls bypass so the server can
|
|
28
|
+
* clear the state. Naturally paired with `preventUnload` for long-running
|
|
29
|
+
* server actions. */
|
|
30
|
+
busy?: boolean;
|
|
25
31
|
}
|
|
26
32
|
/** Build a redirect response (Vm and State omitted; shell navigates the browser). */
|
|
27
33
|
export declare function shellRedirect<TState = unknown>(url: string): ShellResponseBody<TState>;
|
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.
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
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 */,
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.16.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
|
@@ -649,6 +649,18 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
649
649
|
.vms-table__pagination-btn { padding: var(--vms-space-xs) var(--vms-space-sm); font-size: var(--vms-text-sm); }
|
|
650
650
|
.vms-table__pagination-btn:disabled { opacity: 0.45; cursor: default; }
|
|
651
651
|
|
|
652
|
+
/* 0.16.0 — `.vms-busy` is applied by BrowserAdapter when the shell is in a
|
|
653
|
+
busy state (a user-initiated dispatch is in flight OR the server returned
|
|
654
|
+
ShellResponse.busy: true). cursor: wait communicates the state; pointer-
|
|
655
|
+
events: none on interactive descendants makes the lock honest — rapid
|
|
656
|
+
clicks during a round-trip never visually flip checkboxes / depress buttons
|
|
657
|
+
because the click never reaches them. Polls (silent dispatches) bypass the
|
|
658
|
+
class so a long-running action stays continuously locked across many ticks
|
|
659
|
+
without flickering. */
|
|
660
|
+
.vms-busy { cursor: wait; }
|
|
661
|
+
.vms-busy a, .vms-busy button, .vms-busy input, .vms-busy textarea,
|
|
662
|
+
.vms-busy select, .vms-busy [role="button"], .vms-busy [data-id] { pointer-events: none; }
|
|
663
|
+
|
|
652
664
|
/* ── Recommended error-banner pattern ──
|
|
653
665
|
Apps typically render this from the `onError` callback. Not emitted by
|
|
654
666
|
the framework; included here so apps can use the convention without
|