@ashley-shrok/viewmodel-shell 3.5.0 → 3.6.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 +4 -4
- package/dist/index.d.ts +4 -0
- package/package.json +2 -2
- package/styles/default.css +25 -0
package/dist/browser.js
CHANGED
|
@@ -293,7 +293,7 @@ export class BrowserAdapter {
|
|
|
293
293
|
}
|
|
294
294
|
page(n, parent, on) {
|
|
295
295
|
const el = document.createElement("div");
|
|
296
|
-
el.className = `vms-page${n.density === "compact" ? " vms-page--compact" : ""}${n.layout && n.layout !== "stack" ? ` vms-page--${n.layout}` : ""}${n.width ? ` vms-page--${n.width}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}`;
|
|
296
|
+
el.className = `vms-page${n.density === "compact" ? " vms-page--compact" : ""}${n.layout && n.layout !== "stack" ? ` vms-page--${n.layout}` : ""}${n.fill === true ? " vms-page--fill" : ""}${n.width ? ` vms-page--${n.width}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}`;
|
|
297
297
|
if (n.title) {
|
|
298
298
|
const h = document.createElement("h1");
|
|
299
299
|
h.className = "vms-page__title";
|
|
@@ -405,7 +405,7 @@ export class BrowserAdapter {
|
|
|
405
405
|
this.sectionKeyCounter.set(baseKey, ordinal + 1);
|
|
406
406
|
const finalKey = `${baseKey}:${ordinal}`;
|
|
407
407
|
const el = document.createElement("details");
|
|
408
|
-
el.className = `vms-section vms-section--collapsible${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.alignSelf ? ` vms-self--${n.alignSelf}` : ""}${n.maxWidth ? ` vms-maxw--${n.maxWidth}` : ""}`;
|
|
408
|
+
el.className = `vms-section vms-section--collapsible${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.fill === true ? " vms-section--fill" : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.alignSelf ? ` vms-self--${n.alignSelf}` : ""}${n.maxWidth ? ` vms-maxw--${n.maxWidth}` : ""}`;
|
|
409
409
|
el.dataset.sectionKey = finalKey;
|
|
410
410
|
// Initial render is always closed — the post-render restore loop in
|
|
411
411
|
// render() re-applies `open=true` for keys the user had open before.
|
|
@@ -429,7 +429,7 @@ export class BrowserAdapter {
|
|
|
429
429
|
// action — see validateSectionAction in server.ts.
|
|
430
430
|
if (n.link) {
|
|
431
431
|
const a = document.createElement("a");
|
|
432
|
-
a.className = `vms-section vms-section--linked${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.alignSelf ? ` vms-self--${n.alignSelf}` : ""}${n.maxWidth ? ` vms-maxw--${n.maxWidth}` : ""}`;
|
|
432
|
+
a.className = `vms-section vms-section--linked${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.fill === true ? " vms-section--fill" : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.alignSelf ? ` vms-self--${n.alignSelf}` : ""}${n.maxWidth ? ` vms-maxw--${n.maxWidth}` : ""}`;
|
|
433
433
|
a.href = n.link.url;
|
|
434
434
|
// Mirror LinkNode's external-attribute pattern (browser.ts ~line 666)
|
|
435
435
|
// byte-for-byte: target=_blank + rel=noopener noreferrer when external.
|
|
@@ -473,7 +473,7 @@ export class BrowserAdapter {
|
|
|
473
473
|
return;
|
|
474
474
|
}
|
|
475
475
|
const el = document.createElement("section");
|
|
476
|
-
el.className = `vms-section${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.alignSelf ? ` vms-self--${n.alignSelf}` : ""}${n.maxWidth ? ` vms-maxw--${n.maxWidth}` : ""}${n.action ? " vms-section--clickable" : ""}`;
|
|
476
|
+
el.className = `vms-section${n.variant === "card" ? " vms-section--card" : ""}${n.tone ? ` vms-section--${n.tone}` : ""}${n.layout && n.layout !== "stack" ? ` vms-section--${n.layout}` : ""}${n.fill === true ? " vms-section--fill" : ""}${n.arrange ? ` vms-arrange--${n.arrange}` : ""}${n.align ? ` vms-align--${n.align}` : ""}${n.threshold ? ` vms-switch--${n.threshold}` : ""}${n.limit ? ` vms-switch-limit--${n.limit}` : ""}${n.minItem ? ` vms-cards-min--${n.minItem}` : ""}${n.alignSelf ? ` vms-self--${n.alignSelf}` : ""}${n.maxWidth ? ` vms-maxw--${n.maxWidth}` : ""}${n.action ? " vms-section--clickable" : ""}`;
|
|
477
477
|
if (n.heading) {
|
|
478
478
|
const h = document.createElement("h2");
|
|
479
479
|
h.className = "vms-section__heading";
|
package/dist/index.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface PageNode {
|
|
|
83
83
|
density?: "comfortable" | "compact";
|
|
84
84
|
/** 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), "row" (left-aligned wrapping horizontal row; items hug content), "switcher" (N equal items flipping all-row ↔ all-stack atomically at a content-width `threshold` — the negative-flex-basis primitive a grid cannot express; distinct from `cards` auto-fit which passes through intermediate column counts) emit .vms-page--{value}. Closed union (D-01/D-02; sidebar D-28; row D-30; switcher SWITCH-01). */
|
|
85
85
|
layout?: "stack" | "split" | "cards" | "sidebar" | "row" | "switcher";
|
|
86
|
+
/** When true the page fills the viewport height (`height:100dvh`) so a `fill` section inside it can take the leftover space and scroll internally — the full-height app-shell axis (a pinned footer/header with an internally-scrolling body, the Flutter Column+Expanded mechanism). Meant to pair with a `SectionNode.fill` child, which becomes the `flex:1 1 auto; min-height:0; overflow-y:auto` region. Absent/false = normal document flow (byte-identical to today). Orthogonal to `layout`. */
|
|
87
|
+
fill?: boolean;
|
|
86
88
|
/** 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). */
|
|
87
89
|
width?: "wide" | "full";
|
|
88
90
|
/** Main-axis arrangement for `layout:"row"` (the cluster primitive) — maps to `justify-content`. Omitted = no class → the row default (`flex-start`, left-pack) holds = byte-identical to today. Closed union copied verbatim from Jetpack Compose `Arrangement` ∩ Flutter `MainAxisAlignment` (ALIGN-01). Emits .vms-arrange--{value}. */
|
|
@@ -106,6 +108,8 @@ export interface SectionNode {
|
|
|
106
108
|
tone?: "danger" | "warning" | "success" | "info";
|
|
107
109
|
/** 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), "row" (left-aligned wrapping horizontal row; items hug content), "switcher" (N equal items flipping all-row ↔ all-stack atomically at a content-width `threshold` — the negative-flex-basis primitive a grid cannot express; distinct from `cards` auto-fit which passes through intermediate column counts) emit .vms-section--{value}. Closed union (D-01/D-02; sidebar D-28; row D-30; switcher SWITCH-01). */
|
|
108
110
|
layout?: "stack" | "split" | "cards" | "sidebar" | "row" | "switcher";
|
|
111
|
+
/** When true (and inside a `fill` page) this section takes the remaining column height and scrolls internally (`flex:1 1 auto; min-height:0; overflow-y:auto`) — the body region of a full-height app shell (e.g. a chat transcript above a pinned composer). Orthogonal to `layout` — a fill section still arranges its own children via `layout`. Outside a `fill` page it's a harmless no-op (the modifier class is inert without a `100dvh` parent). Absent/false = byte-identical to today. */
|
|
112
|
+
fill?: boolean;
|
|
109
113
|
/** Main-axis arrangement for `layout:"row"` (the cluster primitive) — maps to `justify-content`. Omitted = no class → the row default (`flex-start`, left-pack) holds = byte-identical to today. Closed union copied verbatim from Jetpack Compose `Arrangement` ∩ Flutter `MainAxisAlignment` (ALIGN-01). Emits .vms-arrange--{value}. */
|
|
110
114
|
arrange?: "start" | "center" | "end" | "space-between" | "space-around" | "space-evenly";
|
|
111
115
|
/** Cross-axis alignment for `layout:"row"` (the cluster primitive) — maps to `align-items`. Omitted = no class → the row default (`center`) holds = byte-identical to today. Closed union copied verbatim from Flutter `CrossAxisAlignment` (ALIGN-02). Emits .vms-align--{value}. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "3.
|
|
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
|
|
3
|
+
"version": "3.6.1",
|
|
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 \u2014 a .NET reference backend ships with the repo, but any language can produce the JSON contract.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "ashley-shrok",
|
package/styles/default.css
CHANGED
|
@@ -144,6 +144,31 @@ body {
|
|
|
144
144
|
.vms-page--wide { max-width: var(--vms-page-max-wide); }
|
|
145
145
|
.vms-page--full { max-width: none; }
|
|
146
146
|
|
|
147
|
+
/* ── Fill / full-height app-shell axis ──
|
|
148
|
+
`.vms-page--fill` makes the page fill the viewport height so a `.vms-section--fill`
|
|
149
|
+
child can claim the leftover column height and scroll internally (the
|
|
150
|
+
pinned-footer/header + internally-scrolling body shell — Flutter Column+Expanded).
|
|
151
|
+
`.vms-section--fill` is the Expanded region: flex:1 → take remaining height,
|
|
152
|
+
min-height:0 → allow the flex child to shrink below content so overflow scrolls
|
|
153
|
+
instead of pushing siblings off-screen, overflow-y:auto → scroll internally.
|
|
154
|
+
Orthogonal to layout; outside a fill page the section rule is an inert no-op. */
|
|
155
|
+
.vms-page--fill { height: 100dvh; }
|
|
156
|
+
.vms-section--fill { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
157
|
+
/* Fill makes the host's <body> margin load-bearing: the base `body` rule above
|
|
158
|
+
deliberately leaves margin to the host (one-line `body{margin:0}` convention),
|
|
159
|
+
but a 100dvh page plus the UA's default 8px body margin overflows the viewport
|
|
160
|
+
(100dvh + 16px → ~16px of scroll, a clipped pinned footer). So when — and ONLY
|
|
161
|
+
when — a fill page is present, the framework owns the body reset itself. Scoped
|
|
162
|
+
via :has() so non-fill pages keep the existing host-owns-margin convention. */
|
|
163
|
+
body:has(.vms-page--fill) { margin: 0; }
|
|
164
|
+
/* Fill + sidebar composition: `.vms-page--sidebar` is `flex-wrap: wrap` (for the
|
|
165
|
+
breakpoint-free stacked collapse), so the flex LINE's cross-size is the children's
|
|
166
|
+
natural max, not the container's 100dvh. With `align: stretch` the children then
|
|
167
|
+
stretch to that natural max and burst the fill height (e.g. a long sidebar list
|
|
168
|
+
pushes the page past the viewport). Cap each child at the page height and let it
|
|
169
|
+
scroll internally instead. Scoped to fill+sidebar so no other layout is touched. */
|
|
170
|
+
.vms-page--fill.vms-page--sidebar > * { max-height: 100%; overflow-y: auto; }
|
|
171
|
+
|
|
147
172
|
/* ── Section ── */
|
|
148
173
|
.vms-section { display: flex; flex-direction: column; gap: var(--vms-space-sm); }
|
|
149
174
|
.vms-section__heading {
|