@ashley-shrok/viewmodel-shell 0.9.0 → 0.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/dist/browser.js +40 -10
- package/dist/index.d.ts +14 -1
- package/dist/tui.js +15 -9
- package/package.json +1 -1
- package/styles/default.css +10 -0
package/dist/browser.js
CHANGED
|
@@ -249,14 +249,12 @@ export class BrowserAdapter {
|
|
|
249
249
|
form.className = `vms-form${n.layout && n.layout !== "stack" ? ` vms-form--${n.layout}` : ""}`;
|
|
250
250
|
form.noValidate = true;
|
|
251
251
|
this.kids(n.children, form, on);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
submit
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
e.preventDefault();
|
|
259
|
-
const ctx = { ...(n.submitAction.context ?? {}) };
|
|
252
|
+
// 0.10.0 (#15) — harvest this form's current field values, merge into the
|
|
253
|
+
// given action's context, and dispatch. Factored out of the submit
|
|
254
|
+
// handler so both the default submit AND each buttons[] entry can call it
|
|
255
|
+
// with a DIFFERENT action carrying the SAME live field values.
|
|
256
|
+
const harvest = (base) => {
|
|
257
|
+
const ctx = { ...(base.context ?? {}) };
|
|
260
258
|
const files = {};
|
|
261
259
|
form.querySelectorAll("input:not([type=checkbox]):not([type=file]), textarea").forEach(el => { if (el.name)
|
|
262
260
|
ctx[el.name] = el.value; });
|
|
@@ -277,11 +275,43 @@ export class BrowserAdapter {
|
|
|
277
275
|
if (inp.name && inp.files?.[0])
|
|
278
276
|
files[inp.name] = inp.files[0];
|
|
279
277
|
});
|
|
280
|
-
const action = { name:
|
|
278
|
+
const action = { name: base.name, context: ctx };
|
|
281
279
|
if (Object.keys(files).length > 0)
|
|
282
280
|
action.files = files;
|
|
283
281
|
on(action);
|
|
284
|
-
}
|
|
282
|
+
};
|
|
283
|
+
// Default submit button + Enter-to-submit, only when submitAction is set.
|
|
284
|
+
// (0.10.0: submitAction is now optional — a buttons[]-only form renders
|
|
285
|
+
// no default button, and Enter does not submit at the form level.)
|
|
286
|
+
if (n.submitAction) {
|
|
287
|
+
const submitAction = n.submitAction;
|
|
288
|
+
const submit = document.createElement("button");
|
|
289
|
+
submit.type = "submit";
|
|
290
|
+
submit.className = "vms-button vms-button--primary";
|
|
291
|
+
submit.textContent = n.submitLabel ?? "Submit";
|
|
292
|
+
form.appendChild(submit);
|
|
293
|
+
form.addEventListener("submit", (e) => {
|
|
294
|
+
e.preventDefault();
|
|
295
|
+
harvest(submitAction);
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// No default submit — still neutralize implicit Enter submission so a
|
|
300
|
+
// single-field buttons[]-only form doesn't reload via native submit.
|
|
301
|
+
form.addEventListener("submit", (e) => e.preventDefault());
|
|
302
|
+
}
|
|
303
|
+
// 0.10.0 (#15) — multi-action buttons. Each renders through the normal
|
|
304
|
+
// button() path (so variant + pendingLabel work) but its onAction is
|
|
305
|
+
// wrapped to harvest the form first. We render them in a footer row so
|
|
306
|
+
// they group like the default submit.
|
|
307
|
+
if (n.buttons && n.buttons.length > 0) {
|
|
308
|
+
const row = document.createElement("div");
|
|
309
|
+
row.className = "vms-form__buttons";
|
|
310
|
+
const harvestOn = (action) => harvest(action);
|
|
311
|
+
for (const btn of n.buttons)
|
|
312
|
+
this.button(btn, row, harvestOn);
|
|
313
|
+
form.appendChild(row);
|
|
314
|
+
}
|
|
285
315
|
parent.appendChild(form);
|
|
286
316
|
}
|
|
287
317
|
field(n, parent, on) {
|
package/dist/index.d.ts
CHANGED
|
@@ -68,10 +68,23 @@ export interface ListItemNode {
|
|
|
68
68
|
}
|
|
69
69
|
export interface FormNode {
|
|
70
70
|
type: "form";
|
|
71
|
-
|
|
71
|
+
/** The default submit action — renders an auto submit button + fires on
|
|
72
|
+
* Enter in a text field. OPTIONAL since 0.10.0 (#15): omit it for a form
|
|
73
|
+
* whose only triggers are `buttons[]`. When omitted, no default submit
|
|
74
|
+
* button renders and Enter does not submit at the form level (a
|
|
75
|
+
* FieldNode.action still fires per-field). */
|
|
76
|
+
submitAction?: ActionEvent;
|
|
72
77
|
submitLabel?: string;
|
|
73
78
|
/** Layout preset for the form's controls. Omitted or "stack" = fields stacked (current, no modifier class). "inline" = field row + submit on one line (add/search bar) — emits .vms-form--inline. Closed union (D-29). */
|
|
74
79
|
layout?: "stack" | "inline";
|
|
80
|
+
/** Multi-action submit buttons (#15). Each is a full ButtonNode (so
|
|
81
|
+
* `variant` + `pendingLabel` apply) that, on activation, HARVESTS this
|
|
82
|
+
* form's current field values into its `action.context` and dispatches —
|
|
83
|
+
* the same harvest the default submit performs, but carrying a different
|
|
84
|
+
* action. Mirrors HTML's multiple submit buttons / `formaction`. A plain
|
|
85
|
+
* ButtonNode placed in `children` keeps its no-harvest behavior; only
|
|
86
|
+
* buttons in THIS slot harvest. */
|
|
87
|
+
buttons?: ButtonNode[];
|
|
75
88
|
children: ViewNode[];
|
|
76
89
|
}
|
|
77
90
|
export interface FieldNode {
|
package/dist/tui.js
CHANGED
|
@@ -1034,12 +1034,12 @@ function CopyButtonView({ node, ctx }) {
|
|
|
1034
1034
|
// reads each from the adapter's fieldValues map (falling back to wire), and
|
|
1035
1035
|
// dispatches submitAction with `{ [name]: value, ... }` merged into context.
|
|
1036
1036
|
function FormView({ node, ctx }) {
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
};
|
|
1037
|
+
// 0.10.0 (#15) — harvest this form's current field values, merge into the
|
|
1038
|
+
// given action's context, and dispatch. Generalized from the single-submit
|
|
1039
|
+
// closure so the default submit AND each buttons[] entry can call it with
|
|
1040
|
+
// a DIFFERENT action carrying the SAME live field values.
|
|
1041
|
+
const submitFormWith = (base) => {
|
|
1042
|
+
const merged = { ...(base.context ?? {}) };
|
|
1043
1043
|
const collect = (n) => {
|
|
1044
1044
|
if (n.type === "field") {
|
|
1045
1045
|
const wireValue = n.value ?? "";
|
|
@@ -1058,17 +1058,23 @@ function FormView({ node, ctx }) {
|
|
|
1058
1058
|
};
|
|
1059
1059
|
for (const child of node.children)
|
|
1060
1060
|
collect(child);
|
|
1061
|
-
ctx.onAction({ name:
|
|
1061
|
+
ctx.onAction({ name: base.name, context: merged });
|
|
1062
1062
|
};
|
|
1063
|
+
// Enter-in-a-field submits the default action — only wired when present.
|
|
1064
|
+
const submitAction = node.submitAction;
|
|
1063
1065
|
const childCtx = {
|
|
1064
1066
|
...ctx,
|
|
1065
1067
|
isTopLevel: false,
|
|
1066
|
-
submitForm:
|
|
1068
|
+
submitForm: submitAction ? () => submitFormWith(submitAction) : null,
|
|
1067
1069
|
};
|
|
1070
|
+
// buttons[] (#15) render through ButtonView so variant + pendingLabel work;
|
|
1071
|
+
// their onAction is wrapped to harvest the form first. pendingButtonKey
|
|
1072
|
+
// plumbing (0.8.0) flows through ctx unchanged.
|
|
1073
|
+
const buttonCtx = { ...childCtx, onAction: submitFormWith };
|
|
1068
1074
|
// Layout preset on form: "stack" (default — fields stacked) or "inline"
|
|
1069
1075
|
// (field row + submit on one line, the add-bar/search-bar pattern).
|
|
1070
1076
|
const isInline = node.layout === "inline";
|
|
1071
|
-
return (_jsxs("box", { flexDirection: isInline ? "row" : "column", gap: 1, children: [node.children.map((child, i) => renderNode(child, childCtx, i)), _jsxs("text", { attributes: 1 /* BOLD */, children: ["[ ", node.submitLabel ?? "Submit", " ]"] })] }));
|
|
1077
|
+
return (_jsxs("box", { flexDirection: isInline ? "row" : "column", gap: 1, children: [node.children.map((child, i) => renderNode(child, childCtx, i)), submitAction != null ? (_jsxs("text", { attributes: 1 /* BOLD */, children: ["[ ", node.submitLabel ?? "Submit", " ]"] })) : null, node.buttons && node.buttons.length > 0 ? (_jsx("box", { flexDirection: "row", gap: 2, children: node.buttons.map((btn, i) => (_jsx(ButtonView, { node: btn, ctx: buttonCtx }, `b${i}`))) })) : null] }));
|
|
1072
1078
|
}
|
|
1073
1079
|
// ── field ─────────────────────────────────────────────────────────────────
|
|
1074
1080
|
// Real OpenTUI input/textarea/select wired to the adapter's field state.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.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
|
@@ -231,6 +231,16 @@ body {
|
|
|
231
231
|
flex-wrap: wrap;
|
|
232
232
|
}
|
|
233
233
|
.vms-form--inline > .vms-field { flex: 1 1 12rem; }
|
|
234
|
+
/* Multi-action submit buttons row (0.10.0 / #15) — groups the form's
|
|
235
|
+
buttons[] entries on one line, like a dialog action bar. align-self
|
|
236
|
+
flex-start keeps the row from stretching full-width in the stacked form. */
|
|
237
|
+
.vms-form__buttons {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: row;
|
|
240
|
+
gap: var(--vms-space-sm);
|
|
241
|
+
align-self: flex-start;
|
|
242
|
+
flex-wrap: wrap;
|
|
243
|
+
}
|
|
234
244
|
.vms-field { display: flex; flex-direction: column; gap: var(--vms-space-2xs); }
|
|
235
245
|
.vms-field__label { font-size: var(--vms-text-sm); color: var(--vms-text-muted); }
|
|
236
246
|
.vms-field__input {
|