@ashley-shrok/viewmodel-shell 2.1.0 → 3.0.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 +17 -10
- package/dist/index.d.ts +27 -11
- package/dist/tui.js +23 -13
- package/package.json +1 -1
- package/styles/default.css +68 -34
package/dist/browser.js
CHANGED
|
@@ -116,7 +116,12 @@ export class BrowserAdapter {
|
|
|
116
116
|
el.scrollLeft = left;
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
|
-
window
|
|
119
|
+
// Only restore window scroll when the page was actually scrolled — restoring
|
|
120
|
+
// to (0,0) is a no-op, and skipping it avoids jsdom's noisy "Not implemented:
|
|
121
|
+
// window.scrollTo" virtual-console log in unit tests (jsdom never scrolls, so
|
|
122
|
+
// the captured offsets are 0). Mirrors the `el.scrollTop !== 0` guard above.
|
|
123
|
+
if (winScrollX !== 0 || winScrollY !== 0)
|
|
124
|
+
window.scrollTo(winScrollX, winScrollY);
|
|
120
125
|
// 1.2.0 — restore collapsible-section open state after node() rebuild +
|
|
121
126
|
// after focus/scroll restore. Keys absent from the new tree are
|
|
122
127
|
// naturally dropped (querySelectorAll just doesn't find them); new
|
|
@@ -353,7 +358,7 @@ export class BrowserAdapter {
|
|
|
353
358
|
this.sectionKeyCounter.set(baseKey, ordinal + 1);
|
|
354
359
|
const finalKey = `${baseKey}:${ordinal}`;
|
|
355
360
|
const el = document.createElement("details");
|
|
356
|
-
el.className = `vms-section vms-section--collapsible${n.variant === "card" ? " vms-section--card" : ""}${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}` : ""}`;
|
|
361
|
+
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}` : ""}`;
|
|
357
362
|
el.dataset.sectionKey = finalKey;
|
|
358
363
|
// Initial render is always closed — the post-render restore loop in
|
|
359
364
|
// render() re-applies `open=true` for keys the user had open before.
|
|
@@ -377,7 +382,7 @@ export class BrowserAdapter {
|
|
|
377
382
|
// action — see validateSectionAction in server.ts.
|
|
378
383
|
if (n.link) {
|
|
379
384
|
const a = document.createElement("a");
|
|
380
|
-
a.className = `vms-section vms-section--linked${n.variant === "card" ? " vms-section--card" : ""}${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}` : ""}`;
|
|
385
|
+
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}` : ""}`;
|
|
381
386
|
a.href = n.link.url;
|
|
382
387
|
// Mirror LinkNode's external-attribute pattern (browser.ts ~line 666)
|
|
383
388
|
// byte-for-byte: target=_blank + rel=noopener noreferrer when external.
|
|
@@ -421,7 +426,7 @@ export class BrowserAdapter {
|
|
|
421
426
|
return;
|
|
422
427
|
}
|
|
423
428
|
const el = document.createElement("section");
|
|
424
|
-
el.className = `vms-section${n.variant === "card" ? " vms-section--card" : ""}${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.action ? " vms-section--clickable" : ""}`;
|
|
429
|
+
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.action ? " vms-section--clickable" : ""}`;
|
|
425
430
|
if (n.heading) {
|
|
426
431
|
const h = document.createElement("h2");
|
|
427
432
|
h.className = "vms-section__heading";
|
|
@@ -480,7 +485,7 @@ export class BrowserAdapter {
|
|
|
480
485
|
}
|
|
481
486
|
listItem(n, parent, on) {
|
|
482
487
|
const li = document.createElement("li");
|
|
483
|
-
li.className = `vms-list-item${n.
|
|
488
|
+
li.className = `vms-list-item${n.state ? ` vms-list-item--${n.state}` : ""}${n.tone ? ` vms-list-item--${n.tone}` : ""}`;
|
|
484
489
|
if (n.id)
|
|
485
490
|
li.dataset.id = n.id;
|
|
486
491
|
this.kids(n.children, li, on);
|
|
@@ -769,7 +774,7 @@ export class BrowserAdapter {
|
|
|
769
774
|
button(n, parent, on) {
|
|
770
775
|
const btn = document.createElement("button");
|
|
771
776
|
btn.type = "button";
|
|
772
|
-
btn.className = `vms-button${n.
|
|
777
|
+
btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}`;
|
|
773
778
|
btn.textContent = n.label;
|
|
774
779
|
btn.addEventListener("click", () => {
|
|
775
780
|
// pendingLabel: instant client-side feedback. Swap text + add
|
|
@@ -786,7 +791,7 @@ export class BrowserAdapter {
|
|
|
786
791
|
}
|
|
787
792
|
text(n, parent) {
|
|
788
793
|
const el = document.createElement(n.style === "pre" ? "pre" : "span");
|
|
789
|
-
el.className = `vms-text${n.style ? ` vms-text--${n.style}` : ""}`;
|
|
794
|
+
el.className = `vms-text${n.style ? ` vms-text--${n.style}` : ""}${n.tone ? ` vms-text--${n.tone}` : ""}`;
|
|
790
795
|
el.textContent = n.value;
|
|
791
796
|
parent.appendChild(el);
|
|
792
797
|
}
|
|
@@ -1008,8 +1013,10 @@ export class BrowserAdapter {
|
|
|
1008
1013
|
n.rows.forEach(row => {
|
|
1009
1014
|
const tr = document.createElement("tr");
|
|
1010
1015
|
let rowClass = "vms-table__row";
|
|
1011
|
-
if (row.
|
|
1012
|
-
rowClass += ` vms-table__row--${row.
|
|
1016
|
+
if (row.state)
|
|
1017
|
+
rowClass += ` vms-table__row--${row.state}`;
|
|
1018
|
+
if (row.tone)
|
|
1019
|
+
rowClass += ` vms-table__row--${row.tone}`;
|
|
1013
1020
|
if (row.action)
|
|
1014
1021
|
rowClass += " vms-table__row--clickable";
|
|
1015
1022
|
tr.className = rowClass;
|
|
@@ -1135,7 +1142,7 @@ export class BrowserAdapter {
|
|
|
1135
1142
|
copyButton(n, parent) {
|
|
1136
1143
|
const btn = document.createElement("button");
|
|
1137
1144
|
btn.type = "button";
|
|
1138
|
-
btn.className = `vms-button${n.
|
|
1145
|
+
btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}`;
|
|
1139
1146
|
btn.textContent = n.label ?? "Copy";
|
|
1140
1147
|
btn.addEventListener("click", () => {
|
|
1141
1148
|
const write = navigator.clipboard?.writeText(n.text);
|
package/dist/index.d.ts
CHANGED
|
@@ -88,8 +88,10 @@ export interface PageNode {
|
|
|
88
88
|
export interface SectionNode {
|
|
89
89
|
type: "section";
|
|
90
90
|
heading?: string;
|
|
91
|
-
/** Section surface variant. Omitted = current behavior (no modifier class). "card" emits .vms-section--card. Closed union (D-03). */
|
|
91
|
+
/** Section surface variant — the structural KIND of the section's surface (the single meaning of "variant" framework-wide). Omitted = current behavior (no modifier class). "card" emits .vms-section--card. Closed union (D-03). */
|
|
92
92
|
variant?: "card";
|
|
93
|
+
/** Semantic intent/severity tone — the universal status color axis (orthogonal to `variant`; a section can be a `card` AND `tone:"warning"`). Emits .vms-section--{tone} (subtle tinted surface + colored border, reusing the --vms-error/-warning/-success/-info tokens). Omitted = neutral. Closed union. */
|
|
94
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
93
95
|
/** 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). */
|
|
94
96
|
layout?: "stack" | "split" | "cards" | "sidebar" | "row" | "switcher";
|
|
95
97
|
/** 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}. */
|
|
@@ -186,8 +188,10 @@ export interface ListNode {
|
|
|
186
188
|
export interface ListItemNode {
|
|
187
189
|
type: "list-item";
|
|
188
190
|
id?: string;
|
|
189
|
-
/** Appended as a BEM modifier: vms-list-item--{
|
|
190
|
-
|
|
191
|
+
/** Row lifecycle/selection STATE (NOT severity — that's `tone`). A freeform, app-extensible token; the framework ships styling for `active` (selected), `done`, `disabled`, `high` (priority), `running`, `moving`. Appended as a BEM modifier: vms-list-item--{state}. Orthogonal to `tone` (a row can be `state:"active"` AND `tone:"danger"`). */
|
|
192
|
+
state?: string;
|
|
193
|
+
/** Semantic intent/severity — the universal status tone axis (closed). Emits .vms-list-item--{tone} (colored accent border, reusing the shared tokens). Omitted = neutral. */
|
|
194
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
191
195
|
children: ViewNode[];
|
|
192
196
|
}
|
|
193
197
|
export interface FormNode {
|
|
@@ -251,7 +255,12 @@ export interface ButtonNode {
|
|
|
251
255
|
type: "button";
|
|
252
256
|
label: string;
|
|
253
257
|
action: ActionEvent;
|
|
254
|
-
|
|
258
|
+
/** Visual emphasis (how loud) — `primary` = filled, `secondary` = outline. Orthogonal to `tone` and `size`. Emits .vms-button--{emphasis}. Omitted = the neutral default button. Closed union. */
|
|
259
|
+
emphasis?: "primary" | "secondary";
|
|
260
|
+
/** Semantic intent/severity (what it means) — the universal status color axis, orthogonal to `emphasis`. A destructive primary button is `emphasis:"primary"` + `tone:"danger"`. Emits .vms-button--{tone}. Omitted = neutral. Closed union. */
|
|
261
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
262
|
+
/** Box geometry (padding + font), orthogonal to color/emphasis — the one axis no design system bakes into variant. Emits .vms-button--{size}. Omitted = the default (md) size. Closed union. */
|
|
263
|
+
size?: "sm" | "lg";
|
|
255
264
|
/** Transient label shown from click until the dispatch resolves (response
|
|
256
265
|
* arrives or dispatch errors). Mirrors `CopyButtonNode.copiedLabel`'s
|
|
257
266
|
* lifecycle pattern at a different beat: shown DURING the round-trip
|
|
@@ -263,7 +272,10 @@ export interface ButtonNode {
|
|
|
263
272
|
export interface TextNode {
|
|
264
273
|
type: "text";
|
|
265
274
|
value: string;
|
|
266
|
-
|
|
275
|
+
/** Typography role only (NOT color) — emits .vms-text--{style}. Semantic color moved to `tone` (the old `error`/`warning` style values are now `tone:"danger"`/`tone:"warning"`). Closed union. */
|
|
276
|
+
style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "pre";
|
|
277
|
+
/** Semantic intent/severity color — the universal status tone axis, orthogonal to `style` (a heading can be `tone:"danger"`). Emits .vms-text--{tone}; the tone color wins over a `style` color via source order. Omitted = default text color. Closed union. */
|
|
278
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
267
279
|
}
|
|
268
280
|
export interface LinkNode {
|
|
269
281
|
type: "link";
|
|
@@ -357,7 +369,10 @@ export interface TableRow {
|
|
|
357
369
|
* cell (right). A previous version typed this as `ButtonNode[]` and called
|
|
358
370
|
* the button renderer blindly, silently dropping non-button entries. */
|
|
359
371
|
actions?: (ButtonNode | CheckboxNode)[];
|
|
360
|
-
|
|
372
|
+
/** Row lifecycle STATE (NOT severity — that's `tone`). A freeform, app-extensible token; the framework ships styling for `done`, `disabled`, `running`. Emits .vms-table__row--{state}. Orthogonal to `tone`. */
|
|
373
|
+
state?: string;
|
|
374
|
+
/** Semantic intent/severity — the universal status tone axis (closed). Emits .vms-table__row--{tone} (subtle tinted row background, reusing the shared tokens). Omitted = neutral. */
|
|
375
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
361
376
|
}
|
|
362
377
|
export interface TablePagination {
|
|
363
378
|
/** 1-based current page. */
|
|
@@ -410,11 +425,12 @@ export interface CopyButtonNode {
|
|
|
410
425
|
label?: string;
|
|
411
426
|
/** Ephemeral label shown after a successful copy, reverts after ~1.5 s. Adapter default: "Copied!". */
|
|
412
427
|
copiedLabel?: string;
|
|
413
|
-
/** Visual
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
428
|
+
/** Visual emphasis — mirrors ButtonNode.emphasis. `primary` = filled, `secondary` = outline. Emits .vms-button--{emphasis}. Closed union. */
|
|
429
|
+
emphasis?: "primary" | "secondary";
|
|
430
|
+
/** Semantic intent/severity — mirrors ButtonNode.tone. Emits .vms-button--{tone}. Closed union. */
|
|
431
|
+
tone?: "danger" | "warning" | "success" | "info";
|
|
432
|
+
/** Box geometry — mirrors ButtonNode.size. Emits .vms-button--{size}. Omitted = md. Closed union. */
|
|
433
|
+
size?: "sm" | "lg";
|
|
418
434
|
}
|
|
419
435
|
/**
|
|
420
436
|
* The SwiftUI `ViewThatFits` port. The renderer picks the FIRST child whose
|
package/dist/tui.js
CHANGED
|
@@ -855,14 +855,21 @@ const STYLE_ATTRS = {
|
|
|
855
855
|
body: {},
|
|
856
856
|
muted: { fg: "#888888" },
|
|
857
857
|
strikethrough: { attributes: 16 /* STRIKETHROUGH */, fg: "#888888" },
|
|
858
|
-
error: { fg: "#ff5555" },
|
|
859
|
-
warning: { fg: "#e0a823" },
|
|
860
858
|
pre: { fg: "#cccccc" },
|
|
861
859
|
};
|
|
860
|
+
// Universal semantic-tone → foreground color, shared by text / buttons / list items.
|
|
861
|
+
const TONE_FG = {
|
|
862
|
+
danger: "#ff5555",
|
|
863
|
+
warning: "#e0a823",
|
|
864
|
+
success: "#5fd75f",
|
|
865
|
+
info: "#5fafff",
|
|
866
|
+
};
|
|
862
867
|
function TextView({ node }) {
|
|
863
868
|
const style = node.style ?? "body";
|
|
864
869
|
const attrs = STYLE_ATTRS[style];
|
|
865
|
-
|
|
870
|
+
// tone color wins over the style's fg (mirrors the browser source-order rule).
|
|
871
|
+
const fg = node.tone ? TONE_FG[node.tone] : attrs.fg;
|
|
872
|
+
return (_jsx("text", { ...(attrs.attributes != null ? { attributes: attrs.attributes } : {}), ...(fg != null ? { fg } : {}), children: node.value }));
|
|
866
873
|
}
|
|
867
874
|
// ── link ──────────────────────────────────────────────────────────────────
|
|
868
875
|
// External links emit a real OSC 8 hyperlink (\x1b]8;;url\x07label\x1b]8;;\x07);
|
|
@@ -896,13 +903,16 @@ function ImageView({ node }) {
|
|
|
896
903
|
}
|
|
897
904
|
// ── list / list-item ──────────────────────────────────────────────────────
|
|
898
905
|
// A top-level `list` (direct child of page) is a pane on its own; nested
|
|
899
|
-
// lists scroll as part of their containing section. list-item
|
|
900
|
-
// ("done", "active", …)
|
|
906
|
+
// lists scroll as part of their containing section. list-item `state`
|
|
907
|
+
// ("done", "active", …) maps to a text color; semantic `tone` (danger/…)
|
|
908
|
+
// uses the shared TONE_FG and wins over state.
|
|
901
909
|
const LIST_ITEM_FG = {
|
|
902
910
|
done: "#88cc88",
|
|
903
911
|
active: "#88aaff",
|
|
904
|
-
|
|
905
|
-
|
|
912
|
+
running: "#88aaff",
|
|
913
|
+
moving: "#88aaff",
|
|
914
|
+
disabled: "#888888",
|
|
915
|
+
high: "#d76410",
|
|
906
916
|
};
|
|
907
917
|
function ListView({ node, ctx }) {
|
|
908
918
|
const isPane = ctx.isTopLevel;
|
|
@@ -924,9 +934,9 @@ function ListView({ node, ctx }) {
|
|
|
924
934
|
return (_jsx("scrollbox", { focused: focused, focusable: isPaneFocusable, borderColor: focused ? "#88aaff" : "#555555", focusedBorderColor: "#88aaff", flexGrow: 1, flexShrink: 1, children: _jsx("box", { flexDirection: "column", gap: 0, children: node.children.map((child, i) => renderNode(child, childCtx, i)) }) }));
|
|
925
935
|
}
|
|
926
936
|
function ListItemView({ node, ctx }) {
|
|
927
|
-
const fg = node.
|
|
937
|
+
const fg = node.tone ? TONE_FG[node.tone] : node.state ? LIST_ITEM_FG[node.state] : undefined;
|
|
928
938
|
const childCtx = { ...ctx, isTopLevel: false };
|
|
929
|
-
// The
|
|
939
|
+
// The tint applies to text children inside this item via inherited
|
|
930
940
|
// color (OpenTUI <text fg> wins per-element, so this only affects items
|
|
931
941
|
// whose children don't override). For B2 we just thread the children
|
|
932
942
|
// through unchanged; full per-variant styling lands in B3 (with the focus +
|
|
@@ -1022,8 +1032,8 @@ function TableView({ node, ctx }) {
|
|
|
1022
1032
|
// ── minimum-viable text surface for the rest of the node set ───────────────
|
|
1023
1033
|
// Same as B1 — text only, no interactivity. Full widgets land in B3/B4.
|
|
1024
1034
|
function ButtonView({ node, ctx }) {
|
|
1025
|
-
const fg = node.
|
|
1026
|
-
: node.
|
|
1035
|
+
const fg = node.tone ? TONE_FG[node.tone]
|
|
1036
|
+
: node.emphasis === "primary" ? "#88aaff"
|
|
1027
1037
|
: undefined;
|
|
1028
1038
|
// 0.8.0 (#11) — pendingLabel: when set + this button's key matches the
|
|
1029
1039
|
// adapter's pendingButtonKey, render the pending label instead of the
|
|
@@ -1128,8 +1138,8 @@ function CopyButtonView({ node, ctx }) {
|
|
|
1128
1138
|
// 0.9.0 (#14): variant coloring — mirrors ButtonView's fg derivation
|
|
1129
1139
|
// verbatim. Adapter-medium-adaptation parity: the browser uses CSS
|
|
1130
1140
|
// classes; the TUI uses ANSI fg colors. Same semantic mapping.
|
|
1131
|
-
const fg = node.
|
|
1132
|
-
: node.
|
|
1141
|
+
const fg = node.tone ? TONE_FG[node.tone]
|
|
1142
|
+
: node.emphasis === "primary" ? "#88aaff"
|
|
1133
1143
|
: undefined;
|
|
1134
1144
|
return (_jsxs("text", { ...(fg != null ? { fg } : {}), onMouseDown: () => ctx.copy(node.text), children: ["[ ", label, " ]"] }));
|
|
1135
1145
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ashley-shrok/viewmodel-shell",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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
|
@@ -328,6 +328,19 @@ body {
|
|
|
328
328
|
padding: var(--vms-space-md);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
/* ── Section tone — universal status axis (orthogonal to variant:card): a tinted
|
|
332
|
+
surface + solid tone-colored border, reusing the shared semantic tokens.
|
|
333
|
+
The tint is mixed into the OPAQUE surface (not transparent) so it reads on
|
|
334
|
+
dark themes too — a translucent layer over a dark surface is invisible, which
|
|
335
|
+
is why this must mix with var(--vms-surface). Composes with --card (a card
|
|
336
|
+
reads as warning/danger/etc; border-radius + padding come from --card) and
|
|
337
|
+
also tints a bare section. The solid tone border is the always-visible signal
|
|
338
|
+
on any background. ── */
|
|
339
|
+
.vms-section--danger { background: color-mix(in srgb, var(--vms-error) 14%, var(--vms-surface)); border: 1px solid var(--vms-error); }
|
|
340
|
+
.vms-section--warning { background: color-mix(in srgb, var(--vms-warning) 14%, var(--vms-surface)); border: 1px solid var(--vms-warning); }
|
|
341
|
+
.vms-section--success { background: color-mix(in srgb, var(--vms-success) 14%, var(--vms-surface)); border: 1px solid var(--vms-success); }
|
|
342
|
+
.vms-section--info { background: color-mix(in srgb, var(--vms-info) 14%, var(--vms-surface)); border: 1px solid var(--vms-info); }
|
|
343
|
+
|
|
331
344
|
/* ── Section: clickable (1.4.0 — SectionNode.action click-anywhere primitive) ──
|
|
332
345
|
Mirrors the 1.1.0 .vms-table__row--clickable idiom one level up the tree.
|
|
333
346
|
Hover uses a 1px accent-dim ring (rather than a background swap — cards
|
|
@@ -497,8 +510,15 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
497
510
|
}
|
|
498
511
|
.vms-checkbox__label { margin-left: var(--vms-space-xs); font-size: var(--vms-text-base); }
|
|
499
512
|
|
|
500
|
-
/* ── Buttons
|
|
513
|
+
/* ── Buttons — three orthogonal axes: emphasis (fill) × tone (color) × size
|
|
514
|
+
(geometry). `--_btn-tone` carries the semantic color: it defaults to accent
|
|
515
|
+
and the tone classes override it, so an emphasis fills/outlines in whatever
|
|
516
|
+
tone is set (emphasis:primary + tone:danger = a filled RED button). Box
|
|
517
|
+
metrics (padding/font) live ONLY on the base (= md) and the size classes —
|
|
518
|
+
never on emphasis or tone — so a primary and a danger button are the SAME
|
|
519
|
+
size. ── */
|
|
501
520
|
.vms-button {
|
|
521
|
+
--_btn-tone: var(--vms-accent);
|
|
502
522
|
background: var(--vms-surface-2);
|
|
503
523
|
border: 1px solid var(--vms-border);
|
|
504
524
|
border-radius: var(--vms-radius);
|
|
@@ -507,32 +527,37 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
507
527
|
font-family: var(--vms-font-body);
|
|
508
528
|
font-size: var(--vms-text-base);
|
|
509
529
|
padding: var(--vms-space-sm) var(--vms-space-md);
|
|
530
|
+
align-self: flex-start;
|
|
510
531
|
transition: background var(--vms-t), border-color var(--vms-t), transform var(--vms-t);
|
|
511
532
|
}
|
|
512
533
|
.vms-button:hover { border-color: var(--vms-accent-dim); }
|
|
513
534
|
.vms-button:active { transform: scale(0.98); }
|
|
535
|
+
|
|
536
|
+
/* tone — semantic color. Standalone (no emphasis) = colored text + border on the
|
|
537
|
+
neutral surface; combined with an emphasis it recolors the fill/outline. */
|
|
538
|
+
.vms-button--danger { --_btn-tone: var(--vms-error); color: var(--vms-error); border-color: var(--vms-error); }
|
|
539
|
+
.vms-button--warning { --_btn-tone: var(--vms-warning); color: var(--vms-warning); border-color: var(--vms-warning); }
|
|
540
|
+
.vms-button--success { --_btn-tone: var(--vms-success); color: var(--vms-success); border-color: var(--vms-success); }
|
|
541
|
+
.vms-button--info { --_btn-tone: var(--vms-info); color: var(--vms-info); border-color: var(--vms-info); }
|
|
542
|
+
|
|
543
|
+
/* emphasis — fill treatment, colored by --_btn-tone (declared AFTER tone so the
|
|
544
|
+
filled text color / outline color wins on combination). */
|
|
514
545
|
.vms-button--primary {
|
|
515
|
-
background: var(--
|
|
516
|
-
border-color: var(--
|
|
546
|
+
background: var(--_btn-tone);
|
|
547
|
+
border-color: var(--_btn-tone);
|
|
517
548
|
color: #fff;
|
|
518
|
-
padding: var(--vms-space-sm) var(--vms-space-lg);
|
|
519
|
-
align-self: flex-start;
|
|
520
549
|
}
|
|
521
550
|
.vms-button--primary:hover { filter: brightness(1.1); }
|
|
522
551
|
.vms-button--secondary {
|
|
523
552
|
background: transparent;
|
|
524
|
-
border-color: var(--
|
|
525
|
-
color: var(--
|
|
526
|
-
font-size: var(--vms-text-sm);
|
|
527
|
-
padding: var(--vms-space-2xs) var(--vms-space-sm);
|
|
528
|
-
}
|
|
529
|
-
.vms-button--secondary:hover { border-color: var(--vms-accent); background: var(--vms-accent-glow); }
|
|
530
|
-
.vms-button--danger {
|
|
531
|
-
background: transparent;
|
|
532
|
-
border: 1px solid var(--vms-border);
|
|
533
|
-
color: var(--vms-error);
|
|
553
|
+
border-color: var(--_btn-tone);
|
|
554
|
+
color: var(--_btn-tone);
|
|
534
555
|
}
|
|
535
|
-
.vms-button--
|
|
556
|
+
.vms-button--secondary:hover { background: var(--vms-accent-glow); }
|
|
557
|
+
|
|
558
|
+
/* size — the ONLY axis that touches box metrics (md = base, no class). */
|
|
559
|
+
.vms-button--sm { padding: var(--vms-space-2xs) var(--vms-space-sm); font-size: var(--vms-text-sm); }
|
|
560
|
+
.vms-button--lg { padding: var(--vms-space-sm) var(--vms-space-lg); font-size: var(--vms-text-lg); }
|
|
536
561
|
|
|
537
562
|
/* ── Pending state (0.8.0 / issue #11) ──
|
|
538
563
|
Applied by BrowserAdapter when a ButtonNode with `pendingLabel` is clicked.
|
|
@@ -599,20 +624,23 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
599
624
|
}
|
|
600
625
|
.vms-list-item:hover { border-color: var(--vms-accent-dim); }
|
|
601
626
|
|
|
602
|
-
/*
|
|
603
|
-
|
|
627
|
+
/* State (lifecycle) — done/high are framework-styled; apps can add more
|
|
628
|
+
.vms-list-item--{state} rules. The `high` priority accent goes on border-left. */
|
|
604
629
|
.vms-list-item--done { background: var(--vms-done-bg); border-color: transparent; opacity: 0.75; }
|
|
605
|
-
.vms-list-item--
|
|
630
|
+
.vms-list-item--disabled { opacity: 0.55; }
|
|
606
631
|
.vms-list-item--high { border-left: 3px solid var(--vms-priority-high); }
|
|
632
|
+
.vms-list-item--high:hover { border-left-color: var(--vms-priority-high); }
|
|
633
|
+
|
|
634
|
+
/* Tone (severity) — the universal status axis (was the old critical/warning/...
|
|
635
|
+
variants; critical → danger). Colored left accent, preserved through hover. */
|
|
636
|
+
.vms-list-item--danger { border-left: 3px solid var(--vms-error); background: color-mix(in srgb, var(--vms-error) 4%, transparent); }
|
|
607
637
|
.vms-list-item--warning { border-left: 3px solid var(--vms-warning); }
|
|
608
638
|
.vms-list-item--success { border-left: 3px solid var(--vms-success); }
|
|
609
639
|
.vms-list-item--info { border-left: 3px solid var(--vms-info); }
|
|
610
|
-
|
|
611
|
-
.vms-list-item--
|
|
612
|
-
.vms-list-item--
|
|
613
|
-
.vms-list-item--
|
|
614
|
-
.vms-list-item--success:hover { border-left-color: var(--vms-success); }
|
|
615
|
-
.vms-list-item--info:hover { border-left-color: var(--vms-info); }
|
|
640
|
+
.vms-list-item--danger:hover { border-left-color: var(--vms-error); }
|
|
641
|
+
.vms-list-item--warning:hover { border-left-color: var(--vms-warning); }
|
|
642
|
+
.vms-list-item--success:hover { border-left-color: var(--vms-success); }
|
|
643
|
+
.vms-list-item--info:hover { border-left-color: var(--vms-info); }
|
|
616
644
|
|
|
617
645
|
/* Selection state for master-detail / list-detail patterns (D-27). The
|
|
618
646
|
currently-selected row. Stronger than the semantic hints (selection is
|
|
@@ -631,9 +659,13 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
631
659
|
.vms-text--body { line-height: 1.6; }
|
|
632
660
|
.vms-text--muted { color: var(--vms-text-muted); font-size: var(--vms-text-sm); }
|
|
633
661
|
.vms-text--strikethrough { color: var(--vms-done-text); text-decoration: line-through; }
|
|
634
|
-
.vms-text--error { color: var(--vms-error); font-size: var(--vms-text-base); }
|
|
635
|
-
.vms-text--warning { color: var(--vms-warning); font-size: var(--vms-text-base); }
|
|
636
662
|
.vms-text--pre { font-family: var(--vms-font-mono); white-space: pre; }
|
|
663
|
+
/* tone — semantic color axis (was the old style:"error"/"warning"). Placed after
|
|
664
|
+
the typography classes so a tone color wins over a style color (e.g. muted). */
|
|
665
|
+
.vms-text--danger { color: var(--vms-error); }
|
|
666
|
+
.vms-text--warning { color: var(--vms-warning); }
|
|
667
|
+
.vms-text--success { color: var(--vms-success); }
|
|
668
|
+
.vms-text--info { color: var(--vms-info); }
|
|
637
669
|
|
|
638
670
|
/* ── Link ── */
|
|
639
671
|
.vms-link {
|
|
@@ -731,9 +763,9 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
731
763
|
padding: var(--vms-space-sm) var(--vms-space-lg);
|
|
732
764
|
border-top: 1px solid var(--vms-border);
|
|
733
765
|
}
|
|
734
|
-
/* Buttons inside the footer ignore the
|
|
735
|
-
|
|
736
|
-
.vms-modal__footer .vms-button
|
|
766
|
+
/* Buttons inside the footer ignore the base align-self:flex-start so they sit
|
|
767
|
+
naturally on the right-aligned footer row (all emphasis/tone variants). */
|
|
768
|
+
.vms-modal__footer .vms-button { align-self: auto; }
|
|
737
769
|
|
|
738
770
|
/* ── Table ── */
|
|
739
771
|
.vms-table-wrapper {
|
|
@@ -780,16 +812,18 @@ select.vms-field__input { cursor: pointer; padding-right: var(--vms-space-xl);
|
|
|
780
812
|
outline: 2px solid var(--vms-accent);
|
|
781
813
|
outline-offset: -2px;
|
|
782
814
|
}
|
|
783
|
-
/* Row
|
|
784
|
-
|
|
815
|
+
/* Row STATE (lifecycle) — done/disabled/running framework-styled; apps can add
|
|
816
|
+
more .vms-table__row--{state} rules. */
|
|
785
817
|
.vms-table__row--done { opacity: 0.6; }
|
|
786
818
|
.vms-table__row--disabled { opacity: 0.55; color: var(--vms-text-muted); }
|
|
787
819
|
.vms-table__row--disabled.vms-table__row--clickable { cursor: default; }
|
|
788
820
|
.vms-table__row--disabled.vms-table__row--clickable:hover { background: transparent; }
|
|
789
|
-
.vms-table__row--success { background: color-mix(in srgb, var(--vms-success) 8%, transparent); }
|
|
790
821
|
.vms-table__row--running { background: color-mix(in srgb, var(--vms-accent) 8%, transparent); }
|
|
822
|
+
/* Row TONE (severity) — the universal status axis (was the old critical/danger;
|
|
823
|
+
critical → danger). Tints derive from theme vars (color-mix), so a custom
|
|
824
|
+
:root theme recolors them automatically; no app CSS, no literals. */
|
|
825
|
+
.vms-table__row--success { background: color-mix(in srgb, var(--vms-success) 8%, transparent); }
|
|
791
826
|
.vms-table__row--warning { background: color-mix(in srgb, var(--vms-warning) 9%, transparent); }
|
|
792
|
-
.vms-table__row--critical,
|
|
793
827
|
.vms-table__row--danger { background: color-mix(in srgb, var(--vms-error) 9%, transparent); }
|
|
794
828
|
.vms-table__td { padding: var(--vms-space-sm) var(--vms-space-md); vertical-align: middle; }
|
|
795
829
|
.vms-table__link { color: var(--vms-accent); text-decoration: none; }
|