@ashley-shrok/viewmodel-shell 1.8.0 → 1.10.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/agent-skill.md +12 -0
- package/dist/browser.js +20 -0
- package/dist/index.d.ts +4 -0
- package/dist/server.d.ts +20 -0
- package/dist/server.js +7 -0
- package/package.json +1 -1
- package/styles/default.css +0 -6
package/agent-skill.md
CHANGED
|
@@ -62,6 +62,7 @@ Optional success-path fields, which may appear alone or alongside `vm`/`state`:
|
|
|
62
62
|
| `nextPollIn` | Milliseconds. Schedule a `{"name": "poll"}` dispatch after this delay. |
|
|
63
63
|
| `busy` | Boolean. While `true`, drop user-initiated dispatches. Polls bypass. The next response that omits or sets `false` clears the lock. |
|
|
64
64
|
| `preventUnload` | Boolean. While `true`, treat the page as having unsaved work — warn before navigating away. |
|
|
65
|
+
| `rejected` | A SOFT (domain/validation) rejection — see below. The action was refused, but `vm`/`state` are still returned. |
|
|
65
66
|
|
|
66
67
|
**Failure:**
|
|
67
68
|
|
|
@@ -71,6 +72,17 @@ Optional success-path fields, which may appear alone or alongside `vm`/`state`:
|
|
|
71
72
|
|
|
72
73
|
`errors[].path` and `errors[].code` are optional. HTTP status is 4xx or 5xx on failure. Check `ok` once at the response edge; do not branch on HTTP status.
|
|
73
74
|
|
|
75
|
+
**Soft rejections (`rejected`) — important for wire-driving agents.** `ok:false` means the framework could not give you a view. A *domain/validation* rejection is different: the app refused the action (e.g. "targets must be non-negative", "can't remove the only person") but it is still a normal `ok:true` render that preserves the user's input. So `ok` alone does NOT tell you the action succeeded — **on an `ok:true` response, also check for `rejected`.** When present:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{ "ok": true, "vm": { /* … */ }, "state": { /* … */ },
|
|
79
|
+
"rejected": { "violations": [ { "path": "targets.protein", "message": "must be non-negative" } ] } }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- The write **did not take effect.** Do not treat it as success. Surface the violation(s); `vm`/`state` still hold the input the user typed, so you can correct and re-dispatch.
|
|
83
|
+
- Each violation reuses the `errors[]` entry shape (`{ path?, message, code? }`). **`path` is optional**: present → the violation is bound to that field; **absent → it's a form/action-level rejection** with no single field (like the "only person" case).
|
|
84
|
+
- `rejected` only appears on `ok:true`. It never coexists with `ok:false` (that channel carries no view).
|
|
85
|
+
|
|
74
86
|
## Side-effect verbs
|
|
75
87
|
|
|
76
88
|
`sideEffects[]` entries each carry a `type` discriminator. Built-in verbs as of `viewmodel-shell/1.0`:
|
package/dist/browser.js
CHANGED
|
@@ -419,6 +419,26 @@ export class BrowserAdapter {
|
|
|
419
419
|
// single-field buttons[]-only form doesn't reload via native submit.
|
|
420
420
|
form.addEventListener("submit", (e) => e.preventDefault());
|
|
421
421
|
}
|
|
422
|
+
// Opt-in chat-composer affordance: bare Enter in a descendant textarea
|
|
423
|
+
// dispatches the submit (a textarea otherwise eats Enter as a newline and
|
|
424
|
+
// never submits). Modifier-Enter falls through to a normal newline, and an
|
|
425
|
+
// IME composition Enter (candidate confirmation) must NOT submit. No-op
|
|
426
|
+
// when submitAction is absent. Same dispatch path as the submit button.
|
|
427
|
+
if (n.submitOnEnter && n.submitAction) {
|
|
428
|
+
const submitAction = n.submitAction;
|
|
429
|
+
form.querySelectorAll("textarea").forEach(ta => {
|
|
430
|
+
ta.addEventListener("keydown", (e) => {
|
|
431
|
+
if (e.key !== "Enter")
|
|
432
|
+
return;
|
|
433
|
+
if (e.isComposing || e.keyCode === 229)
|
|
434
|
+
return; // IME candidate confirm — not a send
|
|
435
|
+
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
|
|
436
|
+
return; // newline / shortcut
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
dispatchWithFiles(submitAction);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
}
|
|
422
442
|
if (n.buttons && n.buttons.length > 0) {
|
|
423
443
|
const row = document.createElement("div");
|
|
424
444
|
row.className = "vms-form__buttons";
|
package/dist/index.d.ts
CHANGED
|
@@ -189,6 +189,10 @@ export interface FormNode {
|
|
|
189
189
|
* button, same underlying state. A plain ButtonNode placed in `children`
|
|
190
190
|
* has identical dispatch semantics; the buttons[] slot is a layout hint. */
|
|
191
191
|
buttons?: ButtonNode[];
|
|
192
|
+
/** Opt-in: bare Enter inside a descendant <textarea> dispatches submitAction
|
|
193
|
+
* (chat-composer "Enter sends, Shift/Ctrl/Meta/Alt+Enter = newline"). No-op
|
|
194
|
+
* when submitAction is absent or during IME composition. Default false. */
|
|
195
|
+
submitOnEnter?: boolean;
|
|
192
196
|
children: ViewNode[];
|
|
193
197
|
}
|
|
194
198
|
export interface FieldNode {
|
package/dist/server.d.ts
CHANGED
|
@@ -53,9 +53,29 @@ export interface ShellResponseBody<TState> {
|
|
|
53
53
|
* clear the state. Naturally paired with `preventUnload` for long-running
|
|
54
54
|
* server actions. */
|
|
55
55
|
busy?: boolean;
|
|
56
|
+
/** A SOFT (domain/validation) rejection that rides on an ok:true render —
|
|
57
|
+
* the action was refused but vm/state are still returned so the form keeps
|
|
58
|
+
* the user's input. Distinct from the ok:false + errors[] channel (which
|
|
59
|
+
* carries NO view): ok:false = "no view for you"; ok:true + rejected =
|
|
60
|
+
* "here's your view back, but the action did not take". An agent driving the
|
|
61
|
+
* wire checks `rejected` IN ADDITION to `ok`. Each violation reuses the
|
|
62
|
+
* ErrorEntry {path?, message, code?} shape; `path` is OPTIONAL — a violation
|
|
63
|
+
* with no path is a form/action-level rejection (vs field-bound when set). */
|
|
64
|
+
rejected?: ShellRejection;
|
|
65
|
+
}
|
|
66
|
+
/** Wrapper for a soft-validation rejection on an ok:true response. */
|
|
67
|
+
export interface ShellRejection {
|
|
68
|
+
violations: ErrorEntry[];
|
|
56
69
|
}
|
|
57
70
|
/** Build a redirect response (Vm and State omitted; shell navigates the browser). */
|
|
58
71
|
export declare function shellRedirect<TState = unknown>(url: string): ShellResponseBody<TState>;
|
|
72
|
+
/** Attach a soft-validation rejection to a normal re-render. Spread alongside
|
|
73
|
+
* vm/state — unlike a redirect, a rejection KEEPS the view:
|
|
74
|
+
* return { vm, state, ...shellRejection([{ path: "x", message: "…" }]) };
|
|
75
|
+
* Mirrors the C# `ShellResponse<T>.WithRejection(...)` fluent helper. */
|
|
76
|
+
export declare function shellRejection(violations: ErrorEntry[]): {
|
|
77
|
+
rejected: ShellRejection;
|
|
78
|
+
};
|
|
59
79
|
/** Side-effect factories matching the C# ShellSideEffect static methods. */
|
|
60
80
|
export declare const shellSideEffect: {
|
|
61
81
|
setLocalStorage: (key: string, value: string) => ShellSideEffect;
|
package/dist/server.js
CHANGED
|
@@ -379,6 +379,13 @@ export function parseJsonAction(body) {
|
|
|
379
379
|
export function shellRedirect(url) {
|
|
380
380
|
return { redirect: url };
|
|
381
381
|
}
|
|
382
|
+
/** Attach a soft-validation rejection to a normal re-render. Spread alongside
|
|
383
|
+
* vm/state — unlike a redirect, a rejection KEEPS the view:
|
|
384
|
+
* return { vm, state, ...shellRejection([{ path: "x", message: "…" }]) };
|
|
385
|
+
* Mirrors the C# `ShellResponse<T>.WithRejection(...)` fluent helper. */
|
|
386
|
+
export function shellRejection(violations) {
|
|
387
|
+
return { rejected: { violations } };
|
|
388
|
+
}
|
|
382
389
|
/** Side-effect factories matching the C# ShellSideEffect static methods. */
|
|
383
390
|
export const shellSideEffect = {
|
|
384
391
|
setLocalStorage: (key, value) => ({ type: "set-local-storage", key, value }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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
|
@@ -517,11 +517,6 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
517
517
|
background: var(--vms-accent-glow);
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
-
@keyframes vms-in {
|
|
521
|
-
from { opacity: 0; transform: translateY(4px); }
|
|
522
|
-
to { opacity: 1; transform: translateY(0); }
|
|
523
|
-
}
|
|
524
|
-
|
|
525
520
|
/* ── Text ── */
|
|
526
521
|
.vms-text { flex: 1; line-height: 1.4; word-break: break-word; }
|
|
527
522
|
.vms-text--heading { font-family: var(--vms-font-head); font-size: var(--vms-text-xl); }
|
|
@@ -569,7 +564,6 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
569
564
|
justify-content: center;
|
|
570
565
|
padding: var(--vms-space-md);
|
|
571
566
|
z-index: 1000;
|
|
572
|
-
animation: vms-in 0.15s ease;
|
|
573
567
|
}
|
|
574
568
|
.vms-modal {
|
|
575
569
|
background: var(--vms-surface);
|