@ashley-shrok/viewmodel-shell 1.7.0 → 1.9.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 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "1.7.0",
3
+ "version": "1.9.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",
@@ -323,7 +323,16 @@ body {
323
323
  variable so themes can flip it. */
324
324
  color-scheme: var(--vms-color-scheme);
325
325
  }
326
- .vms-field__input::placeholder { color: var(--vms-text-muted); }
326
+ /* Placeholder is derived from the theme text color at reduced strength
327
+ (the UA-default / Bootstrap mechanism, ~50%), NOT from --vms-text-muted.
328
+ Two reasons: (1) it reads as a faint hint rather than committed text, so an
329
+ empty field isn't mistaken for a filled one even with no visible label;
330
+ (2) it decouples from --vms-text-muted (shared by labels/captions/meta),
331
+ which must stay AA-contrast. Opacity (not a fixed gray) so it fades
332
+ correctly across every light/dark theme. This intentionally trades strict
333
+ placeholder AA contrast for clarity — placeholders should never be the sole
334
+ carrier of meaning; pair inputs with a label. */
335
+ .vms-field__input::placeholder { color: var(--vms-text); opacity: 0.5; }
327
336
  .vms-field__input:focus {
328
337
  border-color: var(--vms-accent);
329
338
  box-shadow: 0 0 0 3px var(--vms-accent-glow);
@@ -508,11 +517,6 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
508
517
  background: var(--vms-accent-glow);
509
518
  }
510
519
 
511
- @keyframes vms-in {
512
- from { opacity: 0; transform: translateY(4px); }
513
- to { opacity: 1; transform: translateY(0); }
514
- }
515
-
516
520
  /* ── Text ── */
517
521
  .vms-text { flex: 1; line-height: 1.4; word-break: break-word; }
518
522
  .vms-text--heading { font-family: var(--vms-font-head); font-size: var(--vms-text-xl); }
@@ -560,7 +564,6 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
560
564
  justify-content: center;
561
565
  padding: var(--vms-space-md);
562
566
  z-index: 1000;
563
- animation: vms-in 0.15s ease;
564
567
  }
565
568
  .vms-modal {
566
569
  background: var(--vms-surface);