@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 +22 -2
- package/dist/index.d.ts +2 -0
- package/dist/server.js +6 -1
- package/package.json +1 -1
- package/styles/default.css +17 -3
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|
package/styles/default.css
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|