@ashley-shrok/viewmodel-shell 0.9.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 +54 -10
- package/dist/index.d.ts +28 -3
- package/dist/tui.d.ts +14 -0
- package/dist/tui.js +58 -9
- package/package.json +1 -1
- package/styles/default.css +24 -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);
|
|
@@ -249,14 +250,12 @@ export class BrowserAdapter {
|
|
|
249
250
|
form.className = `vms-form${n.layout && n.layout !== "stack" ? ` vms-form--${n.layout}` : ""}`;
|
|
250
251
|
form.noValidate = true;
|
|
251
252
|
this.kids(n.children, form, on);
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
submit
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
e.preventDefault();
|
|
259
|
-
const ctx = { ...(n.submitAction.context ?? {}) };
|
|
253
|
+
// 0.10.0 (#15) — harvest this form's current field values, merge into the
|
|
254
|
+
// given action's context, and dispatch. Factored out of the submit
|
|
255
|
+
// handler so both the default submit AND each buttons[] entry can call it
|
|
256
|
+
// with a DIFFERENT action carrying the SAME live field values.
|
|
257
|
+
const harvest = (base) => {
|
|
258
|
+
const ctx = { ...(base.context ?? {}) };
|
|
260
259
|
const files = {};
|
|
261
260
|
form.querySelectorAll("input:not([type=checkbox]):not([type=file]), textarea").forEach(el => { if (el.name)
|
|
262
261
|
ctx[el.name] = el.value; });
|
|
@@ -277,11 +276,43 @@ export class BrowserAdapter {
|
|
|
277
276
|
if (inp.name && inp.files?.[0])
|
|
278
277
|
files[inp.name] = inp.files[0];
|
|
279
278
|
});
|
|
280
|
-
const action = { name:
|
|
279
|
+
const action = { name: base.name, context: ctx };
|
|
281
280
|
if (Object.keys(files).length > 0)
|
|
282
281
|
action.files = files;
|
|
283
282
|
on(action);
|
|
284
|
-
}
|
|
283
|
+
};
|
|
284
|
+
// Default submit button + Enter-to-submit, only when submitAction is set.
|
|
285
|
+
// (0.10.0: submitAction is now optional — a buttons[]-only form renders
|
|
286
|
+
// no default button, and Enter does not submit at the form level.)
|
|
287
|
+
if (n.submitAction) {
|
|
288
|
+
const submitAction = n.submitAction;
|
|
289
|
+
const submit = document.createElement("button");
|
|
290
|
+
submit.type = "submit";
|
|
291
|
+
submit.className = "vms-button vms-button--primary";
|
|
292
|
+
submit.textContent = n.submitLabel ?? "Submit";
|
|
293
|
+
form.appendChild(submit);
|
|
294
|
+
form.addEventListener("submit", (e) => {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
harvest(submitAction);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
// No default submit — still neutralize implicit Enter submission so a
|
|
301
|
+
// single-field buttons[]-only form doesn't reload via native submit.
|
|
302
|
+
form.addEventListener("submit", (e) => e.preventDefault());
|
|
303
|
+
}
|
|
304
|
+
// 0.10.0 (#15) — multi-action buttons. Each renders through the normal
|
|
305
|
+
// button() path (so variant + pendingLabel work) but its onAction is
|
|
306
|
+
// wrapped to harvest the form first. We render them in a footer row so
|
|
307
|
+
// they group like the default submit.
|
|
308
|
+
if (n.buttons && n.buttons.length > 0) {
|
|
309
|
+
const row = document.createElement("div");
|
|
310
|
+
row.className = "vms-form__buttons";
|
|
311
|
+
const harvestOn = (action) => harvest(action);
|
|
312
|
+
for (const btn of n.buttons)
|
|
313
|
+
this.button(btn, row, harvestOn);
|
|
314
|
+
form.appendChild(row);
|
|
315
|
+
}
|
|
285
316
|
parent.appendChild(form);
|
|
286
317
|
}
|
|
287
318
|
field(n, parent, on) {
|
|
@@ -539,6 +570,19 @@ export class BrowserAdapter {
|
|
|
539
570
|
});
|
|
540
571
|
parent.appendChild(nav);
|
|
541
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
|
+
}
|
|
542
586
|
progress(n, parent) {
|
|
543
587
|
const track = document.createElement("div");
|
|
544
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;
|
|
@@ -68,10 +68,23 @@ export interface ListItemNode {
|
|
|
68
68
|
}
|
|
69
69
|
export interface FormNode {
|
|
70
70
|
type: "form";
|
|
71
|
-
|
|
71
|
+
/** The default submit action — renders an auto submit button + fires on
|
|
72
|
+
* Enter in a text field. OPTIONAL since 0.10.0 (#15): omit it for a form
|
|
73
|
+
* whose only triggers are `buttons[]`. When omitted, no default submit
|
|
74
|
+
* button renders and Enter does not submit at the form level (a
|
|
75
|
+
* FieldNode.action still fires per-field). */
|
|
76
|
+
submitAction?: ActionEvent;
|
|
72
77
|
submitLabel?: string;
|
|
73
78
|
/** Layout preset for the form's controls. Omitted or "stack" = fields stacked (current, no modifier class). "inline" = field row + submit on one line (add/search bar) — emits .vms-form--inline. Closed union (D-29). */
|
|
74
79
|
layout?: "stack" | "inline";
|
|
80
|
+
/** Multi-action submit buttons (#15). Each is a full ButtonNode (so
|
|
81
|
+
* `variant` + `pendingLabel` apply) that, on activation, HARVESTS this
|
|
82
|
+
* form's current field values into its `action.context` and dispatches —
|
|
83
|
+
* the same harvest the default submit performs, but carrying a different
|
|
84
|
+
* action. Mirrors HTML's multiple submit buttons / `formaction`. A plain
|
|
85
|
+
* ButtonNode placed in `children` keeps its no-harvest behavior; only
|
|
86
|
+
* buttons in THIS slot harvest. */
|
|
87
|
+
buttons?: ButtonNode[];
|
|
75
88
|
children: ViewNode[];
|
|
76
89
|
}
|
|
77
90
|
export interface FieldNode {
|
|
@@ -118,7 +131,7 @@ export interface ButtonNode {
|
|
|
118
131
|
export interface TextNode {
|
|
119
132
|
type: "text";
|
|
120
133
|
value: string;
|
|
121
|
-
style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "error" | "pre";
|
|
134
|
+
style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "error" | "warning" | "pre";
|
|
122
135
|
}
|
|
123
136
|
export interface LinkNode {
|
|
124
137
|
type: "link";
|
|
@@ -127,6 +140,18 @@ export interface LinkNode {
|
|
|
127
140
|
/** true = open outside current app context (browser: new tab + noopener) */
|
|
128
141
|
external?: boolean;
|
|
129
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
|
+
}
|
|
130
155
|
export interface StatBarNode {
|
|
131
156
|
type: "stat-bar";
|
|
132
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
|
|
@@ -1034,12 +1073,12 @@ function CopyButtonView({ node, ctx }) {
|
|
|
1034
1073
|
// reads each from the adapter's fieldValues map (falling back to wire), and
|
|
1035
1074
|
// dispatches submitAction with `{ [name]: value, ... }` merged into context.
|
|
1036
1075
|
function FormView({ node, ctx }) {
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
};
|
|
1076
|
+
// 0.10.0 (#15) — harvest this form's current field values, merge into the
|
|
1077
|
+
// given action's context, and dispatch. Generalized from the single-submit
|
|
1078
|
+
// closure so the default submit AND each buttons[] entry can call it with
|
|
1079
|
+
// a DIFFERENT action carrying the SAME live field values.
|
|
1080
|
+
const submitFormWith = (base) => {
|
|
1081
|
+
const merged = { ...(base.context ?? {}) };
|
|
1043
1082
|
const collect = (n) => {
|
|
1044
1083
|
if (n.type === "field") {
|
|
1045
1084
|
const wireValue = n.value ?? "";
|
|
@@ -1058,17 +1097,23 @@ function FormView({ node, ctx }) {
|
|
|
1058
1097
|
};
|
|
1059
1098
|
for (const child of node.children)
|
|
1060
1099
|
collect(child);
|
|
1061
|
-
ctx.onAction({ name:
|
|
1100
|
+
ctx.onAction({ name: base.name, context: merged });
|
|
1062
1101
|
};
|
|
1102
|
+
// Enter-in-a-field submits the default action — only wired when present.
|
|
1103
|
+
const submitAction = node.submitAction;
|
|
1063
1104
|
const childCtx = {
|
|
1064
1105
|
...ctx,
|
|
1065
1106
|
isTopLevel: false,
|
|
1066
|
-
submitForm:
|
|
1107
|
+
submitForm: submitAction ? () => submitFormWith(submitAction) : null,
|
|
1067
1108
|
};
|
|
1109
|
+
// buttons[] (#15) render through ButtonView so variant + pendingLabel work;
|
|
1110
|
+
// their onAction is wrapped to harvest the form first. pendingButtonKey
|
|
1111
|
+
// plumbing (0.8.0) flows through ctx unchanged.
|
|
1112
|
+
const buttonCtx = { ...childCtx, onAction: submitFormWith };
|
|
1068
1113
|
// Layout preset on form: "stack" (default — fields stacked) or "inline"
|
|
1069
1114
|
// (field row + submit on one line, the add-bar/search-bar pattern).
|
|
1070
1115
|
const isInline = node.layout === "inline";
|
|
1071
|
-
return (_jsxs("box", { flexDirection: isInline ? "row" : "column", gap: 1, children: [node.children.map((child, i) => renderNode(child, childCtx, i)), _jsxs("text", { attributes: 1 /* BOLD */, children: ["[ ", node.submitLabel ?? "Submit", " ]"] })] }));
|
|
1116
|
+
return (_jsxs("box", { flexDirection: isInline ? "row" : "column", gap: 1, children: [node.children.map((child, i) => renderNode(child, childCtx, i)), submitAction != null ? (_jsxs("text", { attributes: 1 /* BOLD */, children: ["[ ", node.submitLabel ?? "Submit", " ]"] })) : null, node.buttons && node.buttons.length > 0 ? (_jsx("box", { flexDirection: "row", gap: 2, children: node.buttons.map((btn, i) => (_jsx(ButtonView, { node: btn, ctx: buttonCtx }, `b${i}`))) })) : null] }));
|
|
1072
1117
|
}
|
|
1073
1118
|
// ── field ─────────────────────────────────────────────────────────────────
|
|
1074
1119
|
// Real OpenTUI input/textarea/select wired to the adapter's field state.
|
|
@@ -1196,6 +1241,10 @@ function UnsupportedView({ type }) {
|
|
|
1196
1241
|
// test/conformance.tui.test.ts) invokes the components directly. Note that
|
|
1197
1242
|
// the App component is hooks-free — focus state is passed via props with a
|
|
1198
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
|
+
*/
|
|
1199
1248
|
export function renderTree(vm) {
|
|
1200
1249
|
return _jsx(App, { vm: vm, onAction: () => { } });
|
|
1201
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;
|
|
@@ -231,6 +234,16 @@ body {
|
|
|
231
234
|
flex-wrap: wrap;
|
|
232
235
|
}
|
|
233
236
|
.vms-form--inline > .vms-field { flex: 1 1 12rem; }
|
|
237
|
+
/* Multi-action submit buttons row (0.10.0 / #15) — groups the form's
|
|
238
|
+
buttons[] entries on one line, like a dialog action bar. align-self
|
|
239
|
+
flex-start keeps the row from stretching full-width in the stacked form. */
|
|
240
|
+
.vms-form__buttons {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-direction: row;
|
|
243
|
+
gap: var(--vms-space-sm);
|
|
244
|
+
align-self: flex-start;
|
|
245
|
+
flex-wrap: wrap;
|
|
246
|
+
}
|
|
234
247
|
.vms-field { display: flex; flex-direction: column; gap: var(--vms-space-2xs); }
|
|
235
248
|
.vms-field__label { font-size: var(--vms-text-sm); color: var(--vms-text-muted); }
|
|
236
249
|
.vms-field__input {
|
|
@@ -448,6 +461,7 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
448
461
|
.vms-text--muted { color: var(--vms-text-muted); font-size: var(--vms-text-sm); }
|
|
449
462
|
.vms-text--strikethrough { color: var(--vms-done-text); text-decoration: line-through; }
|
|
450
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); }
|
|
451
465
|
.vms-text--pre { font-family: var(--vms-font-mono); white-space: pre; }
|
|
452
466
|
|
|
453
467
|
/* ── Link ── */
|
|
@@ -463,6 +477,15 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
463
477
|
}
|
|
464
478
|
.vms-link:hover { border-bottom-color: var(--vms-accent); }
|
|
465
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
|
+
|
|
466
489
|
/* ── Progress ── */
|
|
467
490
|
.vms-progress { height: 3px; background: var(--vms-surface-2); border-radius: 99px; overflow: hidden; }
|
|
468
491
|
.vms-progress__bar { height: 100%; background: var(--vms-accent); border-radius: 99px; transition: width 0.3s ease; }
|