@ashley-shrok/viewmodel-shell 2.1.0 → 3.0.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/dist/browser.js CHANGED
@@ -353,7 +353,7 @@ export class BrowserAdapter {
353
353
  this.sectionKeyCounter.set(baseKey, ordinal + 1);
354
354
  const finalKey = `${baseKey}:${ordinal}`;
355
355
  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}` : ""}`;
356
+ 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
357
  el.dataset.sectionKey = finalKey;
358
358
  // Initial render is always closed — the post-render restore loop in
359
359
  // render() re-applies `open=true` for keys the user had open before.
@@ -377,7 +377,7 @@ export class BrowserAdapter {
377
377
  // action — see validateSectionAction in server.ts.
378
378
  if (n.link) {
379
379
  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}` : ""}`;
380
+ 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
381
  a.href = n.link.url;
382
382
  // Mirror LinkNode's external-attribute pattern (browser.ts ~line 666)
383
383
  // byte-for-byte: target=_blank + rel=noopener noreferrer when external.
@@ -421,7 +421,7 @@ export class BrowserAdapter {
421
421
  return;
422
422
  }
423
423
  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" : ""}`;
424
+ 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
425
  if (n.heading) {
426
426
  const h = document.createElement("h2");
427
427
  h.className = "vms-section__heading";
@@ -480,7 +480,7 @@ export class BrowserAdapter {
480
480
  }
481
481
  listItem(n, parent, on) {
482
482
  const li = document.createElement("li");
483
- li.className = `vms-list-item${n.variant ? ` vms-list-item--${n.variant}` : ""}`;
483
+ li.className = `vms-list-item${n.state ? ` vms-list-item--${n.state}` : ""}${n.tone ? ` vms-list-item--${n.tone}` : ""}`;
484
484
  if (n.id)
485
485
  li.dataset.id = n.id;
486
486
  this.kids(n.children, li, on);
@@ -769,7 +769,7 @@ export class BrowserAdapter {
769
769
  button(n, parent, on) {
770
770
  const btn = document.createElement("button");
771
771
  btn.type = "button";
772
- btn.className = `vms-button${n.variant ? ` vms-button--${n.variant}` : ""}`;
772
+ btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}`;
773
773
  btn.textContent = n.label;
774
774
  btn.addEventListener("click", () => {
775
775
  // pendingLabel: instant client-side feedback. Swap text + add
@@ -786,7 +786,7 @@ export class BrowserAdapter {
786
786
  }
787
787
  text(n, parent) {
788
788
  const el = document.createElement(n.style === "pre" ? "pre" : "span");
789
- el.className = `vms-text${n.style ? ` vms-text--${n.style}` : ""}`;
789
+ el.className = `vms-text${n.style ? ` vms-text--${n.style}` : ""}${n.tone ? ` vms-text--${n.tone}` : ""}`;
790
790
  el.textContent = n.value;
791
791
  parent.appendChild(el);
792
792
  }
@@ -1008,8 +1008,10 @@ export class BrowserAdapter {
1008
1008
  n.rows.forEach(row => {
1009
1009
  const tr = document.createElement("tr");
1010
1010
  let rowClass = "vms-table__row";
1011
- if (row.variant)
1012
- rowClass += ` vms-table__row--${row.variant}`;
1011
+ if (row.state)
1012
+ rowClass += ` vms-table__row--${row.state}`;
1013
+ if (row.tone)
1014
+ rowClass += ` vms-table__row--${row.tone}`;
1013
1015
  if (row.action)
1014
1016
  rowClass += " vms-table__row--clickable";
1015
1017
  tr.className = rowClass;
@@ -1135,7 +1137,7 @@ export class BrowserAdapter {
1135
1137
  copyButton(n, parent) {
1136
1138
  const btn = document.createElement("button");
1137
1139
  btn.type = "button";
1138
- btn.className = `vms-button${n.variant ? ` vms-button--${n.variant}` : ""}`;
1140
+ btn.className = `vms-button${n.emphasis ? ` vms-button--${n.emphasis}` : ""}${n.tone ? ` vms-button--${n.tone}` : ""}${n.size ? ` vms-button--${n.size}` : ""}`;
1139
1141
  btn.textContent = n.label ?? "Copy";
1140
1142
  btn.addEventListener("click", () => {
1141
1143
  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--{variant} */
190
- variant?: string;
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
- variant?: "primary" | "secondary" | "danger";
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
- style?: "heading" | "subheading" | "body" | "muted" | "strikethrough" | "error" | "warning" | "pre";
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
- variant?: string;
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 variant — mirrors ButtonNode.variant (issue #14). Adapter emits
414
- * `vms-button vms-button--{variant}` (browser) / variant-tinted text
415
- * (TUI), so a copy-button can read distinctly from neighboring default
416
- * buttons. Closed union; omitted = current behavior (no modifier). */
417
- variant?: "primary" | "secondary" | "danger";
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
- return (_jsx("text", { ...(attrs.attributes != null ? { attributes: attrs.attributes } : {}), ...(attrs.fg != null ? { fg: attrs.fg } : {}), children: node.value }));
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 variants
900
- // ("done", "active", …) are mapped to text colors.
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
- error: "#ff5555",
905
- muted: "#888888",
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.variant ? LIST_ITEM_FG[node.variant] : undefined;
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 variant tint applies to text children inside this item via inherited
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.variant === "danger" ? "#ff5555"
1026
- : node.variant === "primary" ? "#88aaff"
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.variant === "danger" ? "#ff5555"
1132
- : node.variant === "primary" ? "#88aaff"
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": "2.1.0",
3
+ "version": "3.0.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",
@@ -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(--vms-accent);
516
- border-color: var(--vms-accent);
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(--vms-accent-dim);
525
- color: var(--vms-accent);
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--danger:hover { background: var(--vms-error-glow); border-color: var(--vms-error); }
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
- /* Common variants — apps can add more by writing .vms-list-item--{name} rules.
603
- Variant accents go on border-left and are preserved through hover. */
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--critical { border-left: 3px solid var(--vms-error); background: rgba(224, 90, 90, 0.04); }
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--critical:hover { border-left-color: var(--vms-error); }
612
- .vms-list-item--high:hover { border-left-color: var(--vms-priority-high); }
613
- .vms-list-item--warning:hover { border-left-color: var(--vms-warning); }
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 framework's default align-self so
735
- they sit naturally on the footer row. */
736
- .vms-modal__footer .vms-button--primary { align-self: auto; padding: var(--vms-space-sm) var(--vms-space-md); }
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 status variantstints derive from theme vars (color-mix), so a
784
- custom :root theme recolors them automatically; no app CSS, no literals. */
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; }