@ashley-shrok/viewmodel-shell 0.10.0 → 0.11.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/README.md +2 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +14 -0
- package/dist/index.d.ts +14 -2
- package/dist/tui.d.ts +14 -0
- package/dist/tui.js +43 -0
- package/package.json +1 -1
- package/styles/default.css +14 -1
- package/styles/themes/light-amber.css +1 -1
- package/styles/themes/light-blue.css +1 -1
- package/styles/themes/light-green.css +1 -1
- package/styles/themes/light-purple.css +1 -1
- package/styles/themes/light-rose.css +1 -1
- package/styles/themes/light-teal.css +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,8 @@ The download endpoint stays auth-gated and the server authorizes in the action h
|
|
|
51
51
|
|
|
52
52
|
## Terminal (TUI)
|
|
53
53
|
|
|
54
|
+
> ⚠️ **Experimental.** The terminal adapter (`@ashley-shrok/viewmodel-shell/tui` + `vms-tui`) is incomplete and under active design — scrolling, keyboard/focus ergonomics, and layout coverage all need more work. Its API and behavior may change or be removed **without a major-version bump**; don't build production workflows on it yet. The browser/server/core packages are stable and unaffected. Constructing a `TuiAdapter` prints a one-time notice — silence it with `VMS_TUI_SILENCE_EXPERIMENTAL=1`.
|
|
55
|
+
|
|
54
56
|
The same backend renders in a terminal — same wire, no backend change, with a real lazygit-style UX: mouse clicks, wheel scroll, per-pane focus cycle, and a context-aware status bar. Point the CLI at any ViewModel Shell endpoint:
|
|
55
57
|
|
|
56
58
|
```bash
|
package/dist/browser.d.ts
CHANGED
package/dist/browser.js
CHANGED
|
@@ -190,6 +190,7 @@ export class BrowserAdapter {
|
|
|
190
190
|
case "button": return this.button(n, parent, on);
|
|
191
191
|
case "text": return this.text(n, parent);
|
|
192
192
|
case "link": return this.link(n, parent);
|
|
193
|
+
case "image": return this.image(n, parent);
|
|
193
194
|
case "stat-bar": return this.statBar(n, parent);
|
|
194
195
|
case "tabs": return this.tabs(n, parent, on);
|
|
195
196
|
case "progress": return this.progress(n, parent);
|
|
@@ -569,6 +570,19 @@ export class BrowserAdapter {
|
|
|
569
570
|
});
|
|
570
571
|
parent.appendChild(nav);
|
|
571
572
|
}
|
|
573
|
+
image(n, parent) {
|
|
574
|
+
const img = document.createElement("img");
|
|
575
|
+
let cls = "vms-image";
|
|
576
|
+
if (n.size)
|
|
577
|
+
cls += ` vms-image--${n.size}`;
|
|
578
|
+
if (n.shape)
|
|
579
|
+
cls += ` vms-image--${n.shape}`;
|
|
580
|
+
img.className = cls;
|
|
581
|
+
img.src = n.src;
|
|
582
|
+
if (n.alt != null)
|
|
583
|
+
img.alt = n.alt;
|
|
584
|
+
parent.appendChild(img);
|
|
585
|
+
}
|
|
572
586
|
progress(n, parent) {
|
|
573
587
|
const track = document.createElement("div");
|
|
574
588
|
track.className = "vms-progress";
|
package/dist/index.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface Adapter {
|
|
|
33
33
|
* async I/O errors surface via onError. */
|
|
34
34
|
saveFile?(data: Blob, filename: string, contentType: string): void | Promise<void>;
|
|
35
35
|
}
|
|
36
|
-
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode;
|
|
36
|
+
export type ViewNode = PageNode | SectionNode | ListNode | ListItemNode | FormNode | FieldNode | CheckboxNode | ButtonNode | TextNode | LinkNode | ImageNode | StatBarNode | TabsNode | ProgressNode | ModalNode | TableNode | CopyButtonNode;
|
|
37
37
|
export interface PageNode {
|
|
38
38
|
type: "page";
|
|
39
39
|
title?: string;
|
|
@@ -131,7 +131,7 @@ export interface ButtonNode {
|
|
|
131
131
|
export interface TextNode {
|
|
132
132
|
type: "text";
|
|
133
133
|
value: string;
|
|
134
|
-
style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "error" | "pre";
|
|
134
|
+
style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "error" | "warning" | "pre";
|
|
135
135
|
}
|
|
136
136
|
export interface LinkNode {
|
|
137
137
|
type: "link";
|
|
@@ -140,6 +140,18 @@ export interface LinkNode {
|
|
|
140
140
|
/** true = open outside current app context (browser: new tab + noopener) */
|
|
141
141
|
external?: boolean;
|
|
142
142
|
}
|
|
143
|
+
export interface ImageNode {
|
|
144
|
+
type: "image";
|
|
145
|
+
/** Image source URL (required). */
|
|
146
|
+
src: string;
|
|
147
|
+
/** Accessibility text. Non-browser adapters (TUI) degrade to this. */
|
|
148
|
+
alt?: string;
|
|
149
|
+
/** Design-system sizing hint → `.vms-image--{size}`. Omit for intrinsic size
|
|
150
|
+
* (capped at 100% of the container). NOT free-form CSS. */
|
|
151
|
+
size?: "small" | "medium" | "large" | "full";
|
|
152
|
+
/** `"circle"` → square-cropped circular image (avatars). */
|
|
153
|
+
shape?: "circle";
|
|
154
|
+
}
|
|
143
155
|
export interface StatBarNode {
|
|
144
156
|
type: "stat-bar";
|
|
145
157
|
stats: Array<{
|
package/dist/tui.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ interface TuiOpts {
|
|
|
8
8
|
* of the available width; the remainder fills with the rest of the children. */
|
|
9
9
|
sidebarFraction?: number;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Renders a ViewModel Shell view tree to a terminal via OpenTUI (Bun runtime).
|
|
13
|
+
*
|
|
14
|
+
* @experimental The terminal adapter is incomplete and under active design —
|
|
15
|
+
* scrolling, keyboard/focus ergonomics, and layout coverage all need more
|
|
16
|
+
* work. Its API and behavior may change or be removed without a major-version
|
|
17
|
+
* bump. Constructing one prints a one-time stderr notice (silence with
|
|
18
|
+
* `VMS_TUI_SILENCE_EXPERIMENTAL=1`). The browser/server/core packages are
|
|
19
|
+
* stable; only `@ashley-shrok/viewmodel-shell/tui` + `vms-tui` are experimental.
|
|
20
|
+
*/
|
|
11
21
|
export declare class TuiAdapter implements Adapter {
|
|
12
22
|
private renderer;
|
|
13
23
|
private root;
|
|
@@ -74,5 +84,9 @@ export declare class TuiAdapter implements Adapter {
|
|
|
74
84
|
* landed (parity with the Ink adapter's _peekSession). */
|
|
75
85
|
_peekSession(key: string): string | undefined;
|
|
76
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* @experimental Part of the experimental terminal target (see {@link TuiAdapter}).
|
|
89
|
+
* A static, non-mounting render path used by the cross-adapter conformance suite.
|
|
90
|
+
*/
|
|
77
91
|
export declare function renderTree(vm: ViewNode): React.ReactNode;
|
|
78
92
|
export {};
|
package/dist/tui.js
CHANGED
|
@@ -3,6 +3,34 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
+
// ─── Experimental notice ───────────────────────────────────────────────────
|
|
7
|
+
// The terminal adapter is EXPERIMENTAL (see the @experimental tag on
|
|
8
|
+
// TuiAdapter). We emit a one-time stderr heads-up the first time a TuiAdapter
|
|
9
|
+
// is constructed in a process — covering both `vms-tui` (which constructs one)
|
|
10
|
+
// and programmatic consumers. Fires once per process; silence with
|
|
11
|
+
// VMS_TUI_SILENCE_EXPERIMENTAL=1 for deliberate users who don't want the nag.
|
|
12
|
+
let experimentalNoticeShown = false;
|
|
13
|
+
function warnExperimental() {
|
|
14
|
+
if (experimentalNoticeShown)
|
|
15
|
+
return;
|
|
16
|
+
experimentalNoticeShown = true;
|
|
17
|
+
if (process.env.VMS_TUI_SILENCE_EXPERIMENTAL)
|
|
18
|
+
return;
|
|
19
|
+
process.stderr.write("[vms-tui] ⚠ The terminal adapter (TuiAdapter) is EXPERIMENTAL: incomplete, " +
|
|
20
|
+
"under-tested, and subject to breaking change or removal without a major-version " +
|
|
21
|
+
"bump. Not recommended for production. The browser/server/core packages are " +
|
|
22
|
+
"stable and unaffected. Silence this notice with VMS_TUI_SILENCE_EXPERIMENTAL=1.\n");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Renders a ViewModel Shell view tree to a terminal via OpenTUI (Bun runtime).
|
|
26
|
+
*
|
|
27
|
+
* @experimental The terminal adapter is incomplete and under active design —
|
|
28
|
+
* scrolling, keyboard/focus ergonomics, and layout coverage all need more
|
|
29
|
+
* work. Its API and behavior may change or be removed without a major-version
|
|
30
|
+
* bump. Constructing one prints a one-time stderr notice (silence with
|
|
31
|
+
* `VMS_TUI_SILENCE_EXPERIMENTAL=1`). The browser/server/core packages are
|
|
32
|
+
* stable; only `@ashley-shrok/viewmodel-shell/tui` + `vms-tui` are experimental.
|
|
33
|
+
*/
|
|
6
34
|
export class TuiAdapter {
|
|
7
35
|
renderer = null;
|
|
8
36
|
root = null;
|
|
@@ -38,6 +66,7 @@ export class TuiAdapter {
|
|
|
38
66
|
// pending state, no per-button cleanup wiring needed).
|
|
39
67
|
pendingButtonKey = null;
|
|
40
68
|
constructor(opts) {
|
|
69
|
+
warnExperimental();
|
|
41
70
|
this.viewport = opts?.viewport ?? "fill";
|
|
42
71
|
const f = opts?.sidebarFraction ?? 1 / 3;
|
|
43
72
|
this.sidebarFraction = Math.min(0.6, Math.max(0.15, f));
|
|
@@ -672,6 +701,7 @@ function renderNode(node, ctx, key) {
|
|
|
672
701
|
case "section": return _jsx(SectionView, { node: node, ctx: ctx }, key);
|
|
673
702
|
case "text": return _jsx(TextView, { node: node }, key);
|
|
674
703
|
case "link": return _jsx(LinkView, { node: node, ctx: ctx }, key);
|
|
704
|
+
case "image": return _jsx(ImageView, { node: node }, key);
|
|
675
705
|
case "list": return _jsx(ListView, { node: node, ctx: ctx }, key);
|
|
676
706
|
case "list-item": return _jsx(ListItemView, { node: node, ctx: ctx }, key);
|
|
677
707
|
case "table": return _jsx(TableView, { node: node, ctx: ctx }, key);
|
|
@@ -775,6 +805,7 @@ const STYLE_ATTRS = {
|
|
|
775
805
|
muted: { fg: "#888888" },
|
|
776
806
|
strikethrough: { attributes: 16 /* STRIKETHROUGH */, fg: "#888888" },
|
|
777
807
|
error: { fg: "#ff5555" },
|
|
808
|
+
warning: { fg: "#e0a823" },
|
|
778
809
|
pre: { fg: "#cccccc" },
|
|
779
810
|
};
|
|
780
811
|
function TextView({ node }) {
|
|
@@ -804,6 +835,14 @@ function LinkView({ node, ctx }) {
|
|
|
804
835
|
: undefined;
|
|
805
836
|
return (_jsx("text", { attributes: 4 /* UNDERLINE */, fg: "#6688cc", ...(onMouseDown ? { onMouseDown } : {}), children: inner }));
|
|
806
837
|
}
|
|
838
|
+
// ── image ─────────────────────────────────────────────────────────────────
|
|
839
|
+
// Terminals can't render raster images, so the TUI degrades to the alt text
|
|
840
|
+
// (the wire's accessibility intent) — the multi-target-safe contract from the
|
|
841
|
+
// ImageNode design. size/shape are browser-only layout hints and are ignored.
|
|
842
|
+
function ImageView({ node }) {
|
|
843
|
+
const alt = node.alt && node.alt.trim().length > 0 ? node.alt : "image";
|
|
844
|
+
return _jsxs("text", { fg: "#888888", children: ["[image: ", alt, "]"] });
|
|
845
|
+
}
|
|
807
846
|
// ── list / list-item ──────────────────────────────────────────────────────
|
|
808
847
|
// A top-level `list` (direct child of page) is a pane on its own; nested
|
|
809
848
|
// lists scroll as part of their containing section. list-item variants
|
|
@@ -1202,6 +1241,10 @@ function UnsupportedView({ type }) {
|
|
|
1202
1241
|
// test/conformance.tui.test.ts) invokes the components directly. Note that
|
|
1203
1242
|
// the App component is hooks-free — focus state is passed via props with a
|
|
1204
1243
|
// safe default — so the walker works without a React reconciler.
|
|
1244
|
+
/**
|
|
1245
|
+
* @experimental Part of the experimental terminal target (see {@link TuiAdapter}).
|
|
1246
|
+
* A static, non-mounting render path used by the cross-adapter conformance suite.
|
|
1247
|
+
*/
|
|
1205
1248
|
export function renderTree(vm) {
|
|
1206
1249
|
return _jsx(App, { vm: vm, onAction: () => { } });
|
|
1207
1250
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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
|
@@ -28,12 +28,15 @@
|
|
|
28
28
|
--vms-done-text: #9090a0;
|
|
29
29
|
--vms-error: #c2453d;
|
|
30
30
|
--vms-error-glow: rgba(194, 69, 61, 0.10);
|
|
31
|
-
--vms-warning: #
|
|
31
|
+
--vms-warning: #8a630d;
|
|
32
32
|
--vms-priority-high: #d76410;
|
|
33
33
|
--vms-success: #2da359;
|
|
34
34
|
--vms-info: #2277dd;
|
|
35
35
|
--vms-radius: 10px;
|
|
36
36
|
--vms-radius-sm: 6px;
|
|
37
|
+
--vms-image-small: 4rem;
|
|
38
|
+
--vms-image-medium: 8rem;
|
|
39
|
+
--vms-image-large: 16rem;
|
|
37
40
|
--vms-font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
38
41
|
--vms-font-head: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
39
42
|
--vms-font-mono: ui-monospace, 'Cascadia Code', 'Fira Code', Menlo, Consolas, 'Liberation Mono', monospace;
|
|
@@ -458,6 +461,7 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
458
461
|
.vms-text--muted { color: var(--vms-text-muted); font-size: var(--vms-text-sm); }
|
|
459
462
|
.vms-text--strikethrough { color: var(--vms-done-text); text-decoration: line-through; }
|
|
460
463
|
.vms-text--error { color: var(--vms-error); font-size: var(--vms-text-base); }
|
|
464
|
+
.vms-text--warning { color: var(--vms-warning); font-size: var(--vms-text-base); }
|
|
461
465
|
.vms-text--pre { font-family: var(--vms-font-mono); white-space: pre; }
|
|
462
466
|
|
|
463
467
|
/* ── Link ── */
|
|
@@ -473,6 +477,15 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
473
477
|
}
|
|
474
478
|
.vms-link:hover { border-bottom-color: var(--vms-accent); }
|
|
475
479
|
|
|
480
|
+
/* ── Image ── */
|
|
481
|
+
.vms-image { max-width: 100%; height: auto; display: block; }
|
|
482
|
+
.vms-image--small { width: var(--vms-image-small); }
|
|
483
|
+
.vms-image--medium { width: var(--vms-image-medium); }
|
|
484
|
+
.vms-image--large { width: var(--vms-image-large); }
|
|
485
|
+
.vms-image--full { width: 100%; }
|
|
486
|
+
/* Avatars: square-crop to a circle (object-fit avoids distortion on non-square sources). */
|
|
487
|
+
.vms-image--circle { border-radius: 50%; aspect-ratio: 1; object-fit: cover; }
|
|
488
|
+
|
|
476
489
|
/* ── Progress ── */
|
|
477
490
|
.vms-progress { height: 3px; background: var(--vms-surface-2); border-radius: 99px; overflow: hidden; }
|
|
478
491
|
.vms-progress__bar { height: 100%; background: var(--vms-accent); border-radius: 99px; transition: width 0.3s ease; }
|