@ashley-shrok/viewmodel-shell 0.6.0 → 0.7.1

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
@@ -25,6 +25,15 @@ export class BrowserAdapter {
25
25
  const focusId = active?.id || null;
26
26
  const selStart = active?.selectionStart ?? null;
27
27
  const selEnd = active?.selectionEnd ?? null;
28
+ // 0.7.1 (#7) — snapshot the WINDOW scroll position alongside element-level
29
+ // scroll. Without this, an action-driven re-render rebuilds the entire
30
+ // subtree and the viewport jumps (to top, or wherever HTMLElement.focus()
31
+ // scrolled the restored-focus element into view). Same preservation
32
+ // contract as element scroll: preserve unless the server explicitly
33
+ // navigates (a redirect IS a navigation, so it correctly does NOT
34
+ // round-trip through render — it goes through navigate()).
35
+ const winScrollX = window.scrollX;
36
+ const winScrollY = window.scrollY;
28
37
  const scrollMap = new Map();
29
38
  this.container.querySelectorAll("[id]").forEach(el => {
30
39
  if (el.scrollTop !== 0 || el.scrollLeft !== 0)
@@ -44,7 +53,11 @@ export class BrowserAdapter {
44
53
  if (focusId) {
45
54
  const el = this.container.querySelector(`#${CSS.escape(focusId)}`);
46
55
  if (el) {
47
- el.focus();
56
+ // 0.7.1 (#7) — preventScroll stops focus() from yanking the viewport
57
+ // to the focused element. The window-scroll restore below still
58
+ // overrides any scroll that snuck in, but preventing the scroll in
59
+ // the first place is the cleaner contract.
60
+ el.focus({ preventScroll: true });
48
61
  if (selStart !== null && selEnd !== null) {
49
62
  try {
50
63
  el.setSelectionRange(selStart, selEnd);
@@ -60,6 +73,10 @@ export class BrowserAdapter {
60
73
  el.scrollLeft = left;
61
74
  }
62
75
  });
76
+ // 0.7.1 (#7) — restore the window scroll LAST so any defensive
77
+ // browser behavior earlier in this method (e.g. a future element
78
+ // bringing itself into view) gets overridden by the snapshot.
79
+ window.scrollTo(winScrollX, winScrollY);
63
80
  }
64
81
  navigate(url) {
65
82
  window.location.href = url;
@@ -186,7 +203,10 @@ export class BrowserAdapter {
186
203
  }
187
204
  page(n, parent, on) {
188
205
  const el = document.createElement("div");
189
- el.className = `vms-page${n.density === "compact" ? " vms-page--compact" : ""}${n.layout && n.layout !== "stack" ? ` vms-page--${n.layout}` : ""}`;
206
+ el.className = `vms-page${n.density === "compact" ? " vms-page--compact" : ""}${n.layout && n.layout !== "stack" ? ` vms-page--${n.layout}` : ""}${
207
+ // 0.7.0 (#13) — width override: "wide" or "full" opt-in via a closed
208
+ // union. Omitted = no modifier class (existing 1080px cap holds).
209
+ n.width ? ` vms-page--${n.width}` : ""}`;
190
210
  if (n.title) {
191
211
  const h = document.createElement("h1");
192
212
  h.className = "vms-page__title";
package/dist/index.d.ts CHANGED
@@ -41,6 +41,8 @@ export interface PageNode {
41
41
  density?: "comfortable" | "compact";
42
42
  /** Layout preset arranging direct children. Omitted or "stack" = current vertical flow (no modifier class). "split" (equal 2-up), "cards" (uniform grid), "sidebar" (thin + wide app shell) emit .vms-page--{value}. Closed union (D-01/D-02; sidebar D-28). */
43
43
  layout?: "stack" | "split" | "cards" | "sidebar";
44
+ /** Page-shell max-width override. Omitted = framework default cap (`--vms-page-max`, 1080px). "wide" = `--vms-page-max-wide` (1440px default), for data-heavy pages with wide tables. "full" = uncapped (max-width: none), for full-bleed dashboards. TUI ignores this (terminals fill naturally). Closed union (D-13 / issue #13). */
45
+ width?: "wide" | "full";
44
46
  children: ViewNode[];
45
47
  }
46
48
  export interface SectionNode {
package/dist/server.js CHANGED
@@ -19,7 +19,12 @@ export function parseFormDataAction(formData) {
19
19
  const state = JSON.parse(stateRaw);
20
20
  const files = {};
21
21
  for (const [key, value] of formData.entries()) {
22
- if (key !== "_action" && key !== "_state" && value instanceof File) {
22
+ // Narrow via typeof, NOT `instanceof File`: @types/node@22.19+ declares
23
+ // its own `File` interface alongside DOM's, and the TS narrowing for
24
+ // `instanceof File` ambiguates between the two on
25
+ // `FormDataEntryValue = string | File`. `typeof !== "string"` narrows
26
+ // the union to File unambiguously and is identical at runtime.
27
+ if (key !== "_action" && key !== "_state" && typeof value !== "string") {
23
28
  files[key] = value;
24
29
  }
25
30
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashley-shrok/viewmodel-shell",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
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",
@@ -54,10 +54,15 @@
54
54
  --vms-text-lg: 1rem;
55
55
  --vms-text-xl: 1.375rem;
56
56
  --vms-text-2xl: 2.25rem;
57
- /* Page shell (D-12) */
58
- --vms-page-max: 1080px;
57
+ /* Page shell (D-12) — additive override seam (issue #13); host/theme-retunable
58
+ via a single :root{} override imported after the theme. The two width
59
+ ratchets — --vms-page-max-wide (1440px default) and --vms-page-max-full
60
+ (none) — back the `.vms-page--wide` / `.vms-page--full` modifier classes
61
+ emitted when PageNode.width is set. */
62
+ --vms-page-max: 1080px;
63
+ --vms-page-max-wide: 1440px;
59
64
  /* Card-grid min item width — additive override seam (D-05); host/theme-retunable */
60
- --vms-card-min: 16rem;
65
+ --vms-card-min: 16rem;
61
66
  }
62
67
 
63
68
  /* ── Page chrome ──
@@ -116,6 +121,15 @@ body {
116
121
  --vms-space-lg: 1rem;
117
122
  }
118
123
 
124
+ /* ── Width override (0.7.0 / issue #13) ──
125
+ `.vms-page--wide` opts a data-heavy page into a wider cap (1440px default)
126
+ without retuning the global --vms-page-max. `.vms-page--full` removes the
127
+ cap entirely (full-bleed). Both compose with --vms-page-max-wide for host
128
+ retunes — set `:root { --vms-page-max-wide: 1280px }` to change the wide
129
+ value app-wide while leaving the framework default alone. */
130
+ .vms-page--wide { max-width: var(--vms-page-max-wide); }
131
+ .vms-page--full { max-width: none; }
132
+
119
133
  /* ── Layout preset: cards (LAYOUT-03, D-04) ──
120
134
  auto-fit intrinsically collapses toward 1 column as the container narrows;
121
135
  min(...,100%) floor prevents single-column overflow on a narrow viewport