@ashley-shrok/viewmodel-shell 3.3.0 → 3.4.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 +61 -1
- package/dist/index.d.ts +33 -0
- package/package.json +1 -1
- package/styles/default.css +29 -0
package/dist/browser.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export declare class BrowserAdapter implements Adapter {
|
|
|
60
60
|
* When `action` is set, it fires on Enter (text-like) or change (select) —
|
|
61
61
|
* the new value is already in state by that point. */
|
|
62
62
|
private field;
|
|
63
|
+
/** Forms-completeness (3.4.0) — apply disabled/readonly to the control and
|
|
64
|
+
* render help + error text below it, wiring aria-describedby / aria-invalid.
|
|
65
|
+
* Runs on the main field path (the hidden + checkbox-FieldNode variants
|
|
66
|
+
* return before this). */
|
|
67
|
+
private decorateField;
|
|
63
68
|
/** CheckboxNode (standalone, immediate-dispatch) — bound boolean; on toggle,
|
|
64
69
|
* write to state then dispatch the action name (if any). */
|
|
65
70
|
private checkbox;
|
package/dist/browser.js
CHANGED
|
@@ -772,8 +772,61 @@ export class BrowserAdapter {
|
|
|
772
772
|
}
|
|
773
773
|
wrapper.appendChild(inp);
|
|
774
774
|
}
|
|
775
|
+
this.decorateField(wrapper, n);
|
|
775
776
|
parent.appendChild(wrapper);
|
|
776
777
|
}
|
|
778
|
+
/** Forms-completeness (3.4.0) — apply disabled/readonly to the control and
|
|
779
|
+
* render help + error text below it, wiring aria-describedby / aria-invalid.
|
|
780
|
+
* Runs on the main field path (the hidden + checkbox-FieldNode variants
|
|
781
|
+
* return before this). */
|
|
782
|
+
decorateField(wrapper, n) {
|
|
783
|
+
const control = wrapper.querySelector(".vms-field__input");
|
|
784
|
+
if (n.disabled) {
|
|
785
|
+
if (control)
|
|
786
|
+
control.disabled = true;
|
|
787
|
+
wrapper.classList.add("vms-field--disabled");
|
|
788
|
+
}
|
|
789
|
+
if (n.readonly && control && "readOnly" in control) {
|
|
790
|
+
control.readOnly = true;
|
|
791
|
+
}
|
|
792
|
+
// Native input constraints — min/max/step on <input>, maxLength on
|
|
793
|
+
// <input>/<textarea>. Strings pass straight to the attribute.
|
|
794
|
+
if (control instanceof HTMLInputElement) {
|
|
795
|
+
if (n.min != null)
|
|
796
|
+
control.min = n.min;
|
|
797
|
+
if (n.max != null)
|
|
798
|
+
control.max = n.max;
|
|
799
|
+
if (n.step != null)
|
|
800
|
+
control.step = n.step;
|
|
801
|
+
}
|
|
802
|
+
if (n.maxLength != null &&
|
|
803
|
+
(control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement)) {
|
|
804
|
+
control.maxLength = n.maxLength;
|
|
805
|
+
}
|
|
806
|
+
const describedBy = [];
|
|
807
|
+
if (n.help != null && n.help !== "") {
|
|
808
|
+
const helpEl = document.createElement("div");
|
|
809
|
+
helpEl.className = "vms-field__help";
|
|
810
|
+
helpEl.id = `vms-${n.name}-help`;
|
|
811
|
+
helpEl.textContent = n.help;
|
|
812
|
+
wrapper.appendChild(helpEl);
|
|
813
|
+
describedBy.push(helpEl.id);
|
|
814
|
+
}
|
|
815
|
+
if (n.error != null && n.error !== "") {
|
|
816
|
+
wrapper.classList.add("vms-field--error");
|
|
817
|
+
const errEl = document.createElement("div");
|
|
818
|
+
errEl.className = "vms-field__error";
|
|
819
|
+
errEl.id = `vms-${n.name}-error`;
|
|
820
|
+
errEl.setAttribute("role", "alert");
|
|
821
|
+
errEl.textContent = n.error;
|
|
822
|
+
wrapper.appendChild(errEl);
|
|
823
|
+
describedBy.push(errEl.id);
|
|
824
|
+
control?.setAttribute("aria-invalid", "true");
|
|
825
|
+
}
|
|
826
|
+
if (control && describedBy.length > 0) {
|
|
827
|
+
control.setAttribute("aria-describedby", describedBy.join(" "));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
777
830
|
/** CheckboxNode (standalone, immediate-dispatch) — bound boolean; on toggle,
|
|
778
831
|
* write to state then dispatch the action name (if any). */
|
|
779
832
|
checkbox(n, parent, on) {
|
|
@@ -809,9 +862,16 @@ export class BrowserAdapter {
|
|
|
809
862
|
button(n, parent, on) {
|
|
810
863
|
const btn = document.createElement("button");
|
|
811
864
|
btn.type = "button";
|
|
812
|
-
btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}${n.width === "full" ? " vms-button--full" : ""}`;
|
|
865
|
+
btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}${n.width === "full" ? " vms-button--full" : ""}${n.disabled ? " vms-button--disabled" : ""}`;
|
|
813
866
|
btn.textContent = n.label;
|
|
867
|
+
if (n.disabled)
|
|
868
|
+
btn.disabled = true;
|
|
814
869
|
btn.addEventListener("click", () => {
|
|
870
|
+
// Forms-completeness (3.4.0): a disabled button never dispatches. (Native
|
|
871
|
+
// `disabled` already suppresses the click, but guard anyway in case the
|
|
872
|
+
// attribute was cleared out-of-band.)
|
|
873
|
+
if (n.disabled)
|
|
874
|
+
return;
|
|
815
875
|
// pendingLabel: instant client-side feedback. Swap text + add
|
|
816
876
|
// .vms-button--pending BEFORE handing off to the dispatcher. On
|
|
817
877
|
// success the next render replaces the button entirely. On dispatch
|
package/dist/index.d.ts
CHANGED
|
@@ -241,6 +241,35 @@ export interface FieldNode {
|
|
|
241
241
|
label?: string;
|
|
242
242
|
placeholder?: string;
|
|
243
243
|
required?: boolean;
|
|
244
|
+
/** Disables the control (greys it out, blocks input, excludes it from form
|
|
245
|
+
* submission). Server-owned. Emits `.vms-field--disabled` on the wrapper +
|
|
246
|
+
* the native `disabled` attribute. Omitted = enabled. */
|
|
247
|
+
disabled?: boolean;
|
|
248
|
+
/** Read-only: the value is shown and submitted but the user can't edit it
|
|
249
|
+
* (distinct from `disabled`, which also greys out + excludes from submit).
|
|
250
|
+
* Text-like inputs + textarea only. Emits the native `readonly` attribute. */
|
|
251
|
+
readonly?: boolean;
|
|
252
|
+
/** Per-field validation error message, rendered inline below the control as
|
|
253
|
+
* `.vms-field__error` (role="alert"); also sets the wrapper's
|
|
254
|
+
* `.vms-field--error`, the control's `aria-invalid="true"`, and wires the
|
|
255
|
+
* message into `aria-describedby`. The view-side complement to the
|
|
256
|
+
* response-level `rejected` channel — set it on the offending field when you
|
|
257
|
+
* build the tree. Omitted = no error shown. */
|
|
258
|
+
error?: string;
|
|
259
|
+
/** Hint/help text rendered below the control as `.vms-field__help` and wired
|
|
260
|
+
* into the control's `aria-describedby`. Omitted = no hint. */
|
|
261
|
+
help?: string;
|
|
262
|
+
/** `min`/`max`/`step` passed through to the native input attribute for
|
|
263
|
+
* `number`/`range`/date-time inputs. STRINGS (HTML-attribute semantics): a
|
|
264
|
+
* numeric bound (`"0"`), a date bound (`"2020-01-01"`), and `step` also
|
|
265
|
+
* accepts `"any"`. Typed as strings (not numbers) so the wire stays
|
|
266
|
+
* byte-identical across backends. Omitted = no constraint. */
|
|
267
|
+
min?: string;
|
|
268
|
+
max?: string;
|
|
269
|
+
step?: string;
|
|
270
|
+
/** Maximum input length (characters) for text-like inputs + textarea →
|
|
271
|
+
* native `maxlength`. Integer. Omitted = no cap. */
|
|
272
|
+
maxLength?: number;
|
|
244
273
|
options?: Array<{
|
|
245
274
|
value: string;
|
|
246
275
|
label: string;
|
|
@@ -279,6 +308,10 @@ export interface ButtonNode {
|
|
|
279
308
|
* `block`, Chakra `width="full"`). Emits `.vms-button--full`. Omitted/`"auto"`
|
|
280
309
|
* = content-width (the default hug). Orthogonal to emphasis/tone/size. */
|
|
281
310
|
width?: "auto" | "full";
|
|
311
|
+
/** Disables the button — greys it out (`.vms-button--disabled` + native
|
|
312
|
+
* `disabled`) and the renderer will NOT dispatch its action on click.
|
|
313
|
+
* Server-owned (e.g. a submit gated on a precondition). Omitted = enabled. */
|
|
314
|
+
disabled?: boolean;
|
|
282
315
|
/** Transient label shown from click until the dispatch resolves (response
|
|
283
316
|
* arrives or dispatch errors). Mirrors `CopyButtonNode.copiedLabel`'s
|
|
284
317
|
* lifecycle pattern at a different beat: shown DURING the round-trip
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.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
|
@@ -494,6 +494,27 @@ body {
|
|
|
494
494
|
textarea.vms-field__input { min-height: 80px; }
|
|
495
495
|
select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl); }
|
|
496
496
|
|
|
497
|
+
/* Forms-completeness (3.4.0) — help/error text + disabled/readonly/error states.
|
|
498
|
+
help is the muted hint; error is the danger message (role="alert"). */
|
|
499
|
+
.vms-field__help { font-size: var(--vms-text-sm); color: var(--vms-text-muted); }
|
|
500
|
+
.vms-field__error { font-size: var(--vms-text-sm); color: var(--vms-error); }
|
|
501
|
+
.vms-field--error .vms-field__input { border-color: var(--vms-error); }
|
|
502
|
+
.vms-field--error .vms-field__input:focus {
|
|
503
|
+
border-color: var(--vms-error);
|
|
504
|
+
box-shadow: 0 0 0 3px var(--vms-error-glow);
|
|
505
|
+
}
|
|
506
|
+
.vms-field__input:disabled,
|
|
507
|
+
.vms-field--disabled .vms-field__input {
|
|
508
|
+
opacity: 0.55;
|
|
509
|
+
cursor: not-allowed;
|
|
510
|
+
}
|
|
511
|
+
.vms-field--disabled .vms-field__label { opacity: 0.7; }
|
|
512
|
+
/* read-only: editable-looking but visibly inert background (not greyed like disabled). */
|
|
513
|
+
.vms-field__input:read-only:not([type="file"]) {
|
|
514
|
+
background: color-mix(in srgb, var(--vms-surface) 70%, var(--vms-border));
|
|
515
|
+
cursor: default;
|
|
516
|
+
}
|
|
517
|
+
|
|
497
518
|
/* Code editor field — monospaced, tab-aware, no spell-check chrome.
|
|
498
519
|
Apps add syntax highlighting via .vms-field--code-{language} hooks. */
|
|
499
520
|
.vms-field__input--code {
|
|
@@ -617,6 +638,14 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
617
638
|
pointer-events: none;
|
|
618
639
|
}
|
|
619
640
|
|
|
641
|
+
/* Forms-completeness (3.4.0) — server-declared disabled button. */
|
|
642
|
+
.vms-button:disabled,
|
|
643
|
+
.vms-button--disabled {
|
|
644
|
+
opacity: 0.5;
|
|
645
|
+
cursor: not-allowed;
|
|
646
|
+
pointer-events: none;
|
|
647
|
+
}
|
|
648
|
+
|
|
620
649
|
/* Inside a list item, a danger button is treated as the row's "X" affordance:
|
|
621
650
|
muted and hidden until the row is hovered, then visible and red on hover. */
|
|
622
651
|
.vms-list-item .vms-button--danger {
|